first commit
This commit is contained in:
14
resources/views/components/action-message.blade.php
Normal file
14
resources/views/components/action-message.blade.php
Normal file
@@ -0,0 +1,14 @@
|
||||
@props([
|
||||
'on',
|
||||
])
|
||||
|
||||
<div
|
||||
x-data="{ shown: false, timeout: null }"
|
||||
x-init="@this.on('{{ $on }}', () => { clearTimeout(timeout); shown = true; timeout = setTimeout(() => { shown = false }, 2000); })"
|
||||
x-show.transition.out.opacity.duration.1500ms="shown"
|
||||
x-transition:leave.opacity.duration.1500ms
|
||||
style="display: none"
|
||||
{{ $attributes->merge(['class' => 'text-sm']) }}
|
||||
>
|
||||
{{ $slot->isEmpty() ? __('Saved.') : $slot }}
|
||||
</div>
|
||||
16
resources/views/components/activity-icon.blade.php
Normal file
16
resources/views/components/activity-icon.blade.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<!-- resources/views/components/activity-icon.blade.php -->
|
||||
@props(['type'])
|
||||
|
||||
@php
|
||||
$icons = [
|
||||
'upload' => 'document-upload',
|
||||
'approval' => 'shield-check',
|
||||
'comment' => 'chat-alt',
|
||||
'update' => 'pencil-alt',
|
||||
'delete' => 'trash'
|
||||
];
|
||||
|
||||
$defaultIcon = 'bell';
|
||||
@endphp
|
||||
|
||||
<x-icons :icon="$icons[$type] ?? $defaultIcon" class="w-5 h-5 text-gray-400" />
|
||||
8
resources/views/components/app-logo-icon.blade.php
Normal file
8
resources/views/components/app-logo-icon.blade.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 42" {{ $attributes }}>
|
||||
<path
|
||||
fill="currentColor"
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M17.2 5.633 8.6.855 0 5.633v26.51l16.2 9 16.2-9v-8.442l7.6-4.223V9.856l-8.6-4.777-8.6 4.777V18.3l-5.6 3.111V5.633ZM38 18.301l-5.6 3.11v-6.157l5.6-3.11V18.3Zm-1.06-7.856-5.54 3.078-5.54-3.079 5.54-3.078 5.54 3.079ZM24.8 18.3v-6.157l5.6 3.111v6.158L24.8 18.3Zm-1 1.732 5.54 3.078-13.14 7.302-5.54-3.078 13.14-7.3v-.002Zm-16.2 7.89 7.6 4.222V38.3L2 30.966V7.92l5.6 3.111v16.892ZM8.6 9.3 3.06 6.222 8.6 3.143l5.54 3.08L8.6 9.3Zm21.8 15.51-13.2 7.334V38.3l13.2-7.334v-6.156ZM9.6 11.034l5.6-3.11v14.6l-5.6 3.11v-14.6Z"
|
||||
/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 714 B |
6
resources/views/components/app-logo.blade.php
Normal file
6
resources/views/components/app-logo.blade.php
Normal file
@@ -0,0 +1,6 @@
|
||||
<div class="flex aspect-square size-8 items-center justify-center rounded-md bg-accent-content text-accent-foreground">
|
||||
<x-app-logo-icon class="size-5 fill-current text-white dark:text-black" />
|
||||
</div>
|
||||
<div class="ms-1 grid flex-1 text-start text-sm">
|
||||
<span class="mb-0.5 truncate leading-none font-semibold">Laravel Starter Kit</span>
|
||||
</div>
|
||||
9
resources/views/components/auth-header.blade.php
Normal file
9
resources/views/components/auth-header.blade.php
Normal file
@@ -0,0 +1,9 @@
|
||||
@props([
|
||||
'title',
|
||||
'description',
|
||||
])
|
||||
|
||||
<div class="flex w-full flex-col text-center">
|
||||
<flux:heading size="xl">{{ $title }}</flux:heading>
|
||||
<flux:subheading>{{ $description }}</flux:subheading>
|
||||
</div>
|
||||
9
resources/views/components/auth-session-status.blade.php
Normal file
9
resources/views/components/auth-session-status.blade.php
Normal file
@@ -0,0 +1,9 @@
|
||||
@props([
|
||||
'status',
|
||||
])
|
||||
|
||||
@if ($status)
|
||||
<div {{ $attributes->merge(['class' => 'font-medium text-sm text-green-600']) }}>
|
||||
{{ $status }}
|
||||
</div>
|
||||
@endif
|
||||
8
resources/views/components/checkbox.blade.php
Normal file
8
resources/views/components/checkbox.blade.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<!-- resources/views/components/checkbox.blade.php -->
|
||||
@props(['disabled' => false])
|
||||
|
||||
<input
|
||||
type="checkbox"
|
||||
{{ $disabled ? 'disabled' : '' }}
|
||||
{!! $attributes->merge(['class' => 'rounded border-gray-300 text-indigo-600 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50'])
|
||||
!!}>
|
||||
27
resources/views/components/confirmation-modal.blade.php
Normal file
27
resources/views/components/confirmation-modal.blade.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<!-- resources/views/components/confirmation-modal.blade.php -->
|
||||
@props(['show' => false])
|
||||
|
||||
<div x-data="{ show: @entangle($attributes->wire('model')) }"
|
||||
x-show="show"
|
||||
class="fixed inset-0 z-50 overflow-y-auto">
|
||||
<!-- Fondo oscuro -->
|
||||
<div class="fixed inset-0 bg-black opacity-50"></div>
|
||||
|
||||
<!-- Contenido del modal -->
|
||||
<div class="min-h-screen flex items-center justify-center p-4">
|
||||
<div @click.away="show = false"
|
||||
class="bg-white rounded-lg overflow-hidden shadow-xl max-w-md w-full">
|
||||
<div class="px-6 py-4 border-b">
|
||||
<h3 class="text-lg font-medium text-gray-900">{{ $title }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="px-6 py-4">
|
||||
{{ $content }}
|
||||
</div>
|
||||
|
||||
<div class="px-6 py-4 bg-gray-50 flex justify-end space-x-4">
|
||||
{{ $footer }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
6
resources/views/components/dropdown-link.blade.php
Normal file
6
resources/views/components/dropdown-link.blade.php
Normal file
@@ -0,0 +1,6 @@
|
||||
@props(['href' => '#'])
|
||||
|
||||
<a {{ $attributes->merge(['class' => 'block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100']) }}
|
||||
href="{{ $href }}">
|
||||
{{ $slot }}
|
||||
</a>
|
||||
22
resources/views/components/folder-item.blade.php
Normal file
22
resources/views/components/folder-item.blade.php
Normal file
@@ -0,0 +1,22 @@
|
||||
@props(['folder', 'level' => 0])
|
||||
|
||||
<li class="pl-{{ $level * 4 }} group">
|
||||
<div class="flex items-center justify-between p-2 hover:bg-gray-50 rounded-lg cursor-pointer"
|
||||
wire:click="$emit('folderSelected', {{ $folder->id }})">
|
||||
<div class="flex items-center">
|
||||
<x-icons icon="folder" class="w-5 h-5 mr-2 text-yellow-500" />
|
||||
<span class="text-sm">{{ $folder->name }}</span>
|
||||
</div>
|
||||
@if($folder->children->isNotEmpty())
|
||||
<x-icons icon="chevron-right" class="w-4 h-4 text-gray-400 transform group-hover:rotate-90 transition-transform" />
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@if($folder->children->isNotEmpty())
|
||||
<ul class="mt-1 space-y-1">
|
||||
@foreach($folder->children as $child)
|
||||
<x-folder-item :folder="$child" :level="$level + 1" />
|
||||
@endforeach
|
||||
</ul>
|
||||
@endif
|
||||
</li>
|
||||
34
resources/views/components/folder-tree.blade.php
Normal file
34
resources/views/components/folder-tree.blade.php
Normal file
@@ -0,0 +1,34 @@
|
||||
@props(['folder', 'selectedFolderId', 'expandedFolders', 'level' => 0])
|
||||
|
||||
<li class="pl-{{ $level * 2 }}">
|
||||
<div class="flex items-center justify-between p-2 rounded hover:bg-gray-100 {{ $folder->id == $selectedFolderId ? 'bg-blue-50 border border-blue-200' : '' }}">
|
||||
<div class="flex items-center flex-1" wire:click="selectFolder({{ $folder->id }})">
|
||||
<button wire:click.prevent="toggleFolder({{ $folder->id }})" class="mr-2">
|
||||
@if(in_array($folder->id, $expandedFolders))
|
||||
<x-icons.chevron-down class="w-4 h-4 text-gray-400" />
|
||||
@else
|
||||
<x-icons.chevron-right class="w-4 h-4 text-gray-400" />
|
||||
@endif
|
||||
</button>
|
||||
<x-icons.folder class="w-5 h-5 mr-2 text-yellow-500" />
|
||||
<span class="cursor-pointer">{{ $folder->name }}</span>
|
||||
</div>
|
||||
<span class="text-sm text-gray-500">
|
||||
{{ $folder->documents_count }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@if(in_array($folder->id, $expandedFolders))
|
||||
<ul class="mt-2 space-y-2">
|
||||
@foreach($folder->children as $child)
|
||||
<x-folder-item
|
||||
:folder="$child"
|
||||
:selectedFolderId="$selectedFolderId"
|
||||
:expandedFolders="$expandedFolders"
|
||||
:level="$level + 1"
|
||||
wire:key="folder-{{ $child->id }}"
|
||||
/>
|
||||
@endforeach
|
||||
</ul>
|
||||
@endif
|
||||
</li>
|
||||
94
resources/views/components/icons.blade.php
Normal file
94
resources/views/components/icons.blade.php
Normal file
@@ -0,0 +1,94 @@
|
||||
@props(['icon' => ''])
|
||||
|
||||
@if($icon === 'dashboard')
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" {{ $attributes }}>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
|
||||
</svg>
|
||||
|
||||
@elseif($icon === 'folder')
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" {{ $attributes }}>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
|
||||
</svg>
|
||||
|
||||
@elseif($icon === 'document')
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" {{ $attributes }}>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
|
||||
@elseif($icon === 'search')
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" {{ $attributes }}>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
|
||||
@elseif($icon === 'chevron-down')
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" {{ $attributes }}>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
|
||||
@elseif($icon === 'user')
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" {{ $attributes }}>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
||||
</svg>
|
||||
|
||||
@elseif($icon === 'logout')
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" {{ $attributes }}>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
|
||||
</svg>
|
||||
|
||||
@elseif($icon === 'eye')
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" {{ $attributes }}>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
||||
</svg>
|
||||
|
||||
@elseif($icon === 'download')
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" {{ $attributes }}>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
|
||||
</svg>
|
||||
|
||||
@elseif($icon === 'storage')
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" {{ $attributes }}>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01" />
|
||||
</svg>
|
||||
|
||||
@elseif($icon === 'arrow-up')
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" {{ $attributes }}>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 10l7-7m0 0l7 7m-7-7v18" />
|
||||
</svg>
|
||||
|
||||
@elseif($icon === 'clock')
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" {{ $attributes }}>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
|
||||
@elseif($icon === 'trash')
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" {{ $attributes }}>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
|
||||
@elseif($icon === 'edit')
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" {{ $attributes }}>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" />
|
||||
</svg>
|
||||
|
||||
@elseif($icon === 'check')
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" {{ $attributes }}>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
|
||||
@elseif($icon === 'x')
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" {{ $attributes }}>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
|
||||
@elseif($icon === 'plus')
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" {{ $attributes }}>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
|
||||
@else
|
||||
<!-- Icono por defecto (question mark circle) -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" {{ $attributes }}>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
@endif
|
||||
4
resources/views/components/icons/upload.blade.php
Normal file
4
resources/views/components/icons/upload.blade.php
Normal file
@@ -0,0 +1,4 @@
|
||||
<!-- resources/views/components/icons/upload.blade.php -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" {{ $attributes }}>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 322 B |
5
resources/views/components/layouts/app.blade.php
Normal file
5
resources/views/components/layouts/app.blade.php
Normal file
@@ -0,0 +1,5 @@
|
||||
<x-layouts.app.sidebar :title="$title ?? null">
|
||||
<flux:main>
|
||||
{{ $slot }}
|
||||
</flux:main>
|
||||
</x-layouts.app.sidebar>
|
||||
124
resources/views/components/layouts/app/header.blade.php
Normal file
124
resources/views/components/layouts/app/header.blade.php
Normal file
@@ -0,0 +1,124 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="dark">
|
||||
<head>
|
||||
@include('partials.head')
|
||||
</head>
|
||||
<body class="min-h-screen bg-white dark:bg-zinc-800">
|
||||
<flux:header container class="border-b border-zinc-200 bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-900">
|
||||
<flux:sidebar.toggle class="lg:hidden" icon="bars-2" inset="left" />
|
||||
|
||||
<a href="{{ route('dashboard') }}" class="ms-2 me-5 flex items-center space-x-2 rtl:space-x-reverse lg:ms-0" wire:navigate>
|
||||
<x-app-logo />
|
||||
</a>
|
||||
|
||||
<flux:navbar class="-mb-px max-lg:hidden">
|
||||
<flux:navbar.item icon="layout-grid" :href="route('dashboard')" :current="request()->routeIs('dashboard')" wire:navigate>
|
||||
{{ __('Dashboard') }}
|
||||
</flux:navbar.item>
|
||||
</flux:navbar>
|
||||
|
||||
<flux:spacer />
|
||||
|
||||
<flux:navbar class="me-1.5 space-x-0.5 rtl:space-x-reverse py-0!">
|
||||
<flux:tooltip :content="__('Search')" position="bottom">
|
||||
<flux:navbar.item class="!h-10 [&>div>svg]:size-5" icon="magnifying-glass" href="#" :label="__('Search')" />
|
||||
</flux:tooltip>
|
||||
<flux:tooltip :content="__('Repository')" position="bottom">
|
||||
<flux:navbar.item
|
||||
class="h-10 max-lg:hidden [&>div>svg]:size-5"
|
||||
icon="folder-git-2"
|
||||
href="https://github.com/laravel/livewire-starter-kit"
|
||||
target="_blank"
|
||||
:label="__('Repository')"
|
||||
/>
|
||||
</flux:tooltip>
|
||||
<flux:tooltip :content="__('Documentation')" position="bottom">
|
||||
<flux:navbar.item
|
||||
class="h-10 max-lg:hidden [&>div>svg]:size-5"
|
||||
icon="book-open-text"
|
||||
href="https://laravel.com/docs/starter-kits"
|
||||
target="_blank"
|
||||
label="Documentation"
|
||||
/>
|
||||
</flux:tooltip>
|
||||
</flux:navbar>
|
||||
|
||||
<!-- Desktop User Menu -->
|
||||
<flux:dropdown position="top" align="end">
|
||||
<flux:profile
|
||||
class="cursor-pointer"
|
||||
:initials="auth()->user()->initials()"
|
||||
/>
|
||||
|
||||
<flux:menu>
|
||||
<flux:menu.radio.group>
|
||||
<div class="p-0 text-sm font-normal">
|
||||
<div class="flex items-center gap-2 px-1 py-1.5 text-start text-sm">
|
||||
<span class="relative flex h-8 w-8 shrink-0 overflow-hidden rounded-lg">
|
||||
<span
|
||||
class="flex h-full w-full items-center justify-center rounded-lg bg-neutral-200 text-black dark:bg-neutral-700 dark:text-white"
|
||||
>
|
||||
{{ auth()->user()->initials() }}
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<div class="grid flex-1 text-start text-sm leading-tight">
|
||||
<span class="truncate font-semibold">{{ auth()->user()->name }}</span>
|
||||
<span class="truncate text-xs">{{ auth()->user()->email }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</flux:menu.radio.group>
|
||||
|
||||
<flux:menu.separator />
|
||||
|
||||
<flux:menu.radio.group>
|
||||
<flux:menu.item :href="route('settings.profile')" icon="cog" wire:navigate>{{ __('Settings') }}</flux:menu.item>
|
||||
</flux:menu.radio.group>
|
||||
|
||||
<flux:menu.separator />
|
||||
|
||||
<form method="POST" action="{{ route('logout') }}" class="w-full">
|
||||
@csrf
|
||||
<flux:menu.item as="button" type="submit" icon="arrow-right-start-on-rectangle" class="w-full">
|
||||
{{ __('Log Out') }}
|
||||
</flux:menu.item>
|
||||
</form>
|
||||
</flux:menu>
|
||||
</flux:dropdown>
|
||||
</flux:header>
|
||||
|
||||
<!-- Mobile Menu -->
|
||||
<flux:sidebar stashable sticky class="lg:hidden border-e border-zinc-200 bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-900">
|
||||
<flux:sidebar.toggle class="lg:hidden" icon="x-mark" />
|
||||
|
||||
<a href="{{ route('dashboard') }}" class="ms-1 flex items-center space-x-2 rtl:space-x-reverse" wire:navigate>
|
||||
<x-app-logo />
|
||||
</a>
|
||||
|
||||
<flux:navlist variant="outline">
|
||||
<flux:navlist.group :heading="__('Platform')">
|
||||
<flux:navlist.item icon="layout-grid" :href="route('dashboard')" :current="request()->routeIs('dashboard')" wire:navigate>
|
||||
{{ __('Dashboard') }}
|
||||
</flux:navlist.item>
|
||||
</flux:navlist.group>
|
||||
</flux:navlist>
|
||||
|
||||
<flux:spacer />
|
||||
|
||||
<flux:navlist variant="outline">
|
||||
<flux:navlist.item icon="folder-git-2" href="https://github.com/laravel/livewire-starter-kit" target="_blank">
|
||||
{{ __('Repository') }}
|
||||
</flux:navlist.item>
|
||||
|
||||
<flux:navlist.item icon="book-open-text" href="https://laravel.com/docs/starter-kits" target="_blank">
|
||||
{{ __('Documentation') }}
|
||||
</flux:navlist.item>
|
||||
</flux:navlist>
|
||||
</flux:sidebar>
|
||||
|
||||
{{ $slot }}
|
||||
|
||||
@fluxScripts
|
||||
</body>
|
||||
</html>
|
||||
132
resources/views/components/layouts/app/sidebar.blade.php
Normal file
132
resources/views/components/layouts/app/sidebar.blade.php
Normal file
@@ -0,0 +1,132 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="dark">
|
||||
<head>
|
||||
@include('partials.head')
|
||||
</head>
|
||||
<body class="min-h-screen bg-white dark:bg-zinc-800">
|
||||
<flux:sidebar sticky stashable class="border-e border-zinc-200 bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-900">
|
||||
<flux:sidebar.toggle class="lg:hidden" icon="x-mark" />
|
||||
|
||||
<a href="{{ route('dashboard') }}" class="me-5 flex items-center space-x-2 rtl:space-x-reverse" wire:navigate>
|
||||
<x-app-logo />
|
||||
</a>
|
||||
|
||||
<flux:navlist variant="outline">
|
||||
<flux:navlist.group :heading="__('Platform')" class="grid">
|
||||
<flux:navlist.item icon="home" :href="route('dashboard')" :current="request()->routeIs('dashboard')" wire:navigate>{{ __('Dashboard') }}</flux:navlist.item>
|
||||
</flux:navlist.group>
|
||||
</flux:navlist>
|
||||
|
||||
<flux:spacer />
|
||||
|
||||
<flux:navlist variant="outline">
|
||||
<flux:navlist.item icon="folder-git-2" href="https://github.com/laravel/livewire-starter-kit" target="_blank">
|
||||
{{ __('Repository') }}
|
||||
</flux:navlist.item>
|
||||
|
||||
<flux:navlist.item icon="book-open-text" href="https://laravel.com/docs/starter-kits" target="_blank">
|
||||
{{ __('Documentation') }}
|
||||
</flux:navlist.item>
|
||||
</flux:navlist>
|
||||
|
||||
<!-- Desktop User Menu -->
|
||||
<flux:dropdown position="bottom" align="start">
|
||||
<flux:profile
|
||||
:name="auth()->user()->name"
|
||||
:initials="auth()->user()->initials()"
|
||||
icon-trailing="chevrons-up-down"
|
||||
/>
|
||||
|
||||
<flux:menu class="w-[220px]">
|
||||
<flux:menu.radio.group>
|
||||
<div class="p-0 text-sm font-normal">
|
||||
<div class="flex items-center gap-2 px-1 py-1.5 text-start text-sm">
|
||||
<span class="relative flex h-8 w-8 shrink-0 overflow-hidden rounded-lg">
|
||||
<span
|
||||
class="flex h-full w-full items-center justify-center rounded-lg bg-neutral-200 text-black dark:bg-neutral-700 dark:text-white"
|
||||
>
|
||||
{{ auth()->user()->initials() }}
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<div class="grid flex-1 text-start text-sm leading-tight">
|
||||
<span class="truncate font-semibold">{{ auth()->user()->name }}</span>
|
||||
<span class="truncate text-xs">{{ auth()->user()->email }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</flux:menu.radio.group>
|
||||
|
||||
<flux:menu.separator />
|
||||
|
||||
<flux:menu.radio.group>
|
||||
<flux:menu.item :href="route('settings.profile')" icon="cog" wire:navigate>{{ __('Settings') }}</flux:menu.item>
|
||||
</flux:menu.radio.group>
|
||||
|
||||
<flux:menu.separator />
|
||||
|
||||
<form method="POST" action="{{ route('logout') }}" class="w-full">
|
||||
@csrf
|
||||
<flux:menu.item as="button" type="submit" icon="arrow-right-start-on-rectangle" class="w-full">
|
||||
{{ __('Log Out') }}
|
||||
</flux:menu.item>
|
||||
</form>
|
||||
</flux:menu>
|
||||
</flux:dropdown>
|
||||
</flux:sidebar>
|
||||
|
||||
<!-- Mobile User Menu -->
|
||||
<flux:header class="lg:hidden">
|
||||
<flux:sidebar.toggle class="lg:hidden" icon="bars-2" inset="left" />
|
||||
|
||||
<flux:spacer />
|
||||
|
||||
<flux:dropdown position="top" align="end">
|
||||
<flux:profile
|
||||
:initials="auth()->user()->initials()"
|
||||
icon-trailing="chevron-down"
|
||||
/>
|
||||
|
||||
<flux:menu>
|
||||
<flux:menu.radio.group>
|
||||
<div class="p-0 text-sm font-normal">
|
||||
<div class="flex items-center gap-2 px-1 py-1.5 text-start text-sm">
|
||||
<span class="relative flex h-8 w-8 shrink-0 overflow-hidden rounded-lg">
|
||||
<span
|
||||
class="flex h-full w-full items-center justify-center rounded-lg bg-neutral-200 text-black dark:bg-neutral-700 dark:text-white"
|
||||
>
|
||||
{{ auth()->user()->initials() }}
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<div class="grid flex-1 text-start text-sm leading-tight">
|
||||
<span class="truncate font-semibold">{{ auth()->user()->name }}</span>
|
||||
<span class="truncate text-xs">{{ auth()->user()->email }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</flux:menu.radio.group>
|
||||
|
||||
<flux:menu.separator />
|
||||
|
||||
<flux:menu.radio.group>
|
||||
<flux:menu.item :href="route('settings.profile')" icon="cog" wire:navigate>{{ __('Settings') }}</flux:menu.item>
|
||||
</flux:menu.radio.group>
|
||||
|
||||
<flux:menu.separator />
|
||||
|
||||
<form method="POST" action="{{ route('logout') }}" class="w-full">
|
||||
@csrf
|
||||
<flux:menu.item as="button" type="submit" icon="arrow-right-start-on-rectangle" class="w-full">
|
||||
{{ __('Log Out') }}
|
||||
</flux:menu.item>
|
||||
</form>
|
||||
</flux:menu>
|
||||
</flux:dropdown>
|
||||
</flux:header>
|
||||
|
||||
{{ $slot }}
|
||||
|
||||
@fluxScripts
|
||||
</body>
|
||||
</html>
|
||||
3
resources/views/components/layouts/auth.blade.php
Normal file
3
resources/views/components/layouts/auth.blade.php
Normal file
@@ -0,0 +1,3 @@
|
||||
<x-layouts.auth.simple :title="$title ?? null">
|
||||
{{ $slot }}
|
||||
</x-layouts.auth.simple>
|
||||
26
resources/views/components/layouts/auth/card.blade.php
Normal file
26
resources/views/components/layouts/auth/card.blade.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="dark">
|
||||
<head>
|
||||
@include('partials.head')
|
||||
</head>
|
||||
<body class="min-h-screen bg-neutral-100 antialiased dark:bg-linear-to-b dark:from-neutral-950 dark:to-neutral-900">
|
||||
<div class="bg-muted flex min-h-svh flex-col items-center justify-center gap-6 p-6 md:p-10">
|
||||
<div class="flex w-full max-w-md flex-col gap-6">
|
||||
<a href="{{ route('home') }}" class="flex flex-col items-center gap-2 font-medium" wire:navigate>
|
||||
<span class="flex h-9 w-9 items-center justify-center rounded-md">
|
||||
<x-app-logo-icon class="size-9 fill-current text-black dark:text-white" />
|
||||
</span>
|
||||
|
||||
<span class="sr-only">{{ config('app.name', 'Laravel') }}</span>
|
||||
</a>
|
||||
|
||||
<div class="flex flex-col gap-6">
|
||||
<div class="rounded-xl border bg-white dark:bg-stone-950 dark:border-stone-800 text-stone-800 shadow-xs">
|
||||
<div class="px-10 py-8">{{ $slot }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@fluxScripts
|
||||
</body>
|
||||
</html>
|
||||
22
resources/views/components/layouts/auth/simple.blade.php
Normal file
22
resources/views/components/layouts/auth/simple.blade.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="dark">
|
||||
<head>
|
||||
@include('partials.head')
|
||||
</head>
|
||||
<body class="min-h-screen bg-white antialiased dark:bg-linear-to-b dark:from-neutral-950 dark:to-neutral-900">
|
||||
<div class="bg-background flex min-h-svh flex-col items-center justify-center gap-6 p-6 md:p-10">
|
||||
<div class="flex w-full max-w-sm flex-col gap-2">
|
||||
<a href="{{ route('home') }}" class="flex flex-col items-center gap-2 font-medium" wire:navigate>
|
||||
<span class="flex h-9 w-9 mb-1 items-center justify-center rounded-md">
|
||||
<x-app-logo-icon class="size-9 fill-current text-black dark:text-white" />
|
||||
</span>
|
||||
<span class="sr-only">{{ config('app.name', 'Laravel') }}</span>
|
||||
</a>
|
||||
<div class="flex flex-col gap-6">
|
||||
{{ $slot }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@fluxScripts
|
||||
</body>
|
||||
</html>
|
||||
43
resources/views/components/layouts/auth/split.blade.php
Normal file
43
resources/views/components/layouts/auth/split.blade.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="dark">
|
||||
<head>
|
||||
@include('partials.head')
|
||||
</head>
|
||||
<body class="min-h-screen bg-white antialiased dark:bg-linear-to-b dark:from-neutral-950 dark:to-neutral-900">
|
||||
<div class="relative grid h-dvh flex-col items-center justify-center px-8 sm:px-0 lg:max-w-none lg:grid-cols-2 lg:px-0">
|
||||
<div class="bg-muted relative hidden h-full flex-col p-10 text-white lg:flex dark:border-e dark:border-neutral-800">
|
||||
<div class="absolute inset-0 bg-neutral-900"></div>
|
||||
<a href="{{ route('home') }}" class="relative z-20 flex items-center text-lg font-medium" wire:navigate>
|
||||
<span class="flex h-10 w-10 items-center justify-center rounded-md">
|
||||
<x-app-logo-icon class="me-2 h-7 fill-current text-white" />
|
||||
</span>
|
||||
{{ config('app.name', 'Laravel') }}
|
||||
</a>
|
||||
|
||||
@php
|
||||
[$message, $author] = str(Illuminate\Foundation\Inspiring::quotes()->random())->explode('-');
|
||||
@endphp
|
||||
|
||||
<div class="relative z-20 mt-auto">
|
||||
<blockquote class="space-y-2">
|
||||
<flux:heading size="lg">“{{ trim($message) }}”</flux:heading>
|
||||
<footer><flux:heading>{{ trim($author) }}</flux:heading></footer>
|
||||
</blockquote>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full lg:p-8">
|
||||
<div class="mx-auto flex w-full flex-col justify-center space-y-6 sm:w-[350px]">
|
||||
<a href="{{ route('home') }}" class="z-20 flex flex-col items-center gap-2 font-medium lg:hidden" wire:navigate>
|
||||
<span class="flex h-9 w-9 items-center justify-center rounded-md">
|
||||
<x-app-logo-icon class="size-9 fill-current text-black dark:text-white" />
|
||||
</span>
|
||||
|
||||
<span class="sr-only">{{ config('app.name', 'Laravel') }}</span>
|
||||
</a>
|
||||
{{ $slot }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@fluxScripts
|
||||
</body>
|
||||
</html>
|
||||
40
resources/views/components/multiselect.blade.php
Normal file
40
resources/views/components/multiselect.blade.php
Normal file
@@ -0,0 +1,40 @@
|
||||
@props([
|
||||
'options' => [],
|
||||
'selected' => [],
|
||||
'name' => 'multiselect',
|
||||
'id' => 'multiselect-' . Str::random(5),
|
||||
'wireModel' => null // Nuevo prop para el modelo Livewire
|
||||
])
|
||||
|
||||
<div
|
||||
x-data="{
|
||||
open: false,
|
||||
selected: @if($wireModel) @entangle($wireModel) @else {{ json_encode($selected) }} @endif
|
||||
}"
|
||||
class="relative"
|
||||
<div
|
||||
@click="open = !open"
|
||||
class="cursor-pointer p-2 border rounded-lg flex items-center justify-between"
|
||||
>
|
||||
<span x-text="selected.length ? selected.join(', ') : 'Seleccione opciones'"></span>
|
||||
<x-icons icon="chevron-down" class="w-4 h-4 ml-2" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
x-show="open"
|
||||
@click.away="open = false"
|
||||
class="absolute z-50 w-full mt-1 bg-white border rounded-lg shadow-lg max-h-60 overflow-auto"
|
||||
>
|
||||
<template x-for="(option, index) in {{ json_encode($options) }}">
|
||||
<label class="flex items-center px-4 py-2 hover:bg-gray-100">
|
||||
<input
|
||||
type="checkbox"
|
||||
x-model="selected"
|
||||
:value="option.value"
|
||||
class="rounded border-gray-300 text-blue-600 shadow-sm focus:ring-blue-500"
|
||||
>
|
||||
<span x-text="option.label" class="ml-2"></span>
|
||||
</label>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
9
resources/views/components/nav-link.blade.php
Normal file
9
resources/views/components/nav-link.blade.php
Normal file
@@ -0,0 +1,9 @@
|
||||
@props(['href', 'active' => false])
|
||||
|
||||
<a href="{{ $href }}" {{ $attributes->class([
|
||||
'flex items-center px-4 py-2 text-sm transition-colors',
|
||||
'bg-gray-900 text-white' => $active,
|
||||
'text-gray-300 hover:bg-gray-700 hover:text-white' => !$active
|
||||
]) }}>
|
||||
{{ $slot }}
|
||||
</a>
|
||||
12
resources/views/components/placeholder-pattern.blade.php
Normal file
12
resources/views/components/placeholder-pattern.blade.php
Normal file
@@ -0,0 +1,12 @@
|
||||
@props([
|
||||
'id' => uniqid(),
|
||||
])
|
||||
|
||||
<svg {{ $attributes }} fill="none">
|
||||
<defs>
|
||||
<pattern id="pattern-{{ $id }}" x="0" y="0" width="8" height="8" patternUnits="userSpaceOnUse">
|
||||
<path d="M-1 5L5 -1M3 9L8.5 3.5" stroke-width="0.5"></path>
|
||||
</pattern>
|
||||
</defs>
|
||||
<rect stroke="none" fill="url(#pattern-{{ $id }})" width="100%" height="100%"></rect>
|
||||
</svg>
|
||||
6
resources/views/components/select.blade.php
Normal file
6
resources/views/components/select.blade.php
Normal file
@@ -0,0 +1,6 @@
|
||||
<!-- resources/views/components/select.blade.php -->
|
||||
@props(['disabled' => false])
|
||||
|
||||
<select {{ $disabled ? 'disabled' : '' }} {!! $attributes->merge(['class' => 'rounded-md shadow-sm border-gray-300 focus:border-blue-300 focus:ring focus:ring-blue-200 focus:ring-opacity-50']) !!}>
|
||||
{{ $slot }}
|
||||
</select>
|
||||
20
resources/views/components/settings/layout.blade.php
Normal file
20
resources/views/components/settings/layout.blade.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<div class="flex items-start max-md:flex-col">
|
||||
<div class="me-10 w-full pb-4 md:w-[220px]">
|
||||
<flux:navlist>
|
||||
<flux:navlist.item :href="route('settings.profile')" wire:navigate>{{ __('Profile') }}</flux:navlist.item>
|
||||
<flux:navlist.item :href="route('settings.password')" wire:navigate>{{ __('Password') }}</flux:navlist.item>
|
||||
<flux:navlist.item :href="route('settings.appearance')" wire:navigate>{{ __('Appearance') }}</flux:navlist.item>
|
||||
</flux:navlist>
|
||||
</div>
|
||||
|
||||
<flux:separator class="md:hidden" />
|
||||
|
||||
<div class="flex-1 self-stretch max-md:pt-6">
|
||||
<flux:heading>{{ $heading ?? '' }}</flux:heading>
|
||||
<flux:subheading>{{ $subheading ?? '' }}</flux:subheading>
|
||||
|
||||
<div class="mt-5 w-full max-w-lg">
|
||||
{{ $slot }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
30
resources/views/components/stats-card.blade.php
Normal file
30
resources/views/components/stats-card.blade.php
Normal file
@@ -0,0 +1,30 @@
|
||||
@props([
|
||||
'title',
|
||||
'value',
|
||||
'icon', // ← Añade esta línea
|
||||
'color',
|
||||
'trend' => null, // Valor por defecto: null
|
||||
'trendColor' => 'text-gray-500' // Color por defecto
|
||||
])
|
||||
|
||||
<div class="bg-white p-6 rounded-lg shadow">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm text-gray-500">{{ $title }}</p>
|
||||
<p class="text-3xl font-bold">{{ $value }}</p>
|
||||
</div>
|
||||
<div class="{{ $color }} p-3 rounded-full">
|
||||
@if($icon === 'folder')
|
||||
<x-icons icon="folder" class="w-8 h-8 text-white" />
|
||||
@elseif($icon === 'document')
|
||||
<x-icons icon = "document" class="w-8 h-8 text-white" />
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<span class="text-sm {{ $trendColor }}">
|
||||
<x-icons icon="arrow-up" class="w-4 h-4 inline" /> {{ $trend }}%
|
||||
</span>
|
||||
<span class="text-sm text-gray-500">desde el último mes</span>
|
||||
</div>
|
||||
</div>
|
||||
14
resources/views/components/status-badge.blade.php
Normal file
14
resources/views/components/status-badge.blade.php
Normal file
@@ -0,0 +1,14 @@
|
||||
@props(['status'])
|
||||
|
||||
@php
|
||||
$colors = [
|
||||
'pending' => 'bg-yellow-100 text-yellow-800',
|
||||
'approved' => 'bg-green-100 text-green-800',
|
||||
'rejected' => 'bg-red-100 text-red-800',
|
||||
'in_review' => 'bg-blue-100 text-blue-800'
|
||||
];
|
||||
@endphp
|
||||
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium {{ $colors[$status] }}">
|
||||
{{ strtoupper($status) }}
|
||||
</span>
|
||||
4
resources/views/components/textarea.blade.php
Normal file
4
resources/views/components/textarea.blade.php
Normal file
@@ -0,0 +1,4 @@
|
||||
<!-- resources/views/components/textarea.blade.php -->
|
||||
@props(['disabled' => false])
|
||||
|
||||
<textarea {{ $disabled ? 'disabled' : '' }} {!! $attributes->merge(['class' => 'rounded-md shadow-sm border-gray-300 focus:border-blue-300 focus:ring focus:ring-blue-200 focus:ring-opacity-50']) !!}>{{ $slot }}</textarea>
|
||||
62
resources/views/components/toolbar.blade.php
Normal file
62
resources/views/components/toolbar.blade.php
Normal file
@@ -0,0 +1,62 @@
|
||||
<!-- resources/views/components/toolbar.blade.php -->
|
||||
<div class="flex items-center justify-between mb-4 space-x-4">
|
||||
<!-- Grupo izquierdo: Acciones principales -->
|
||||
<div class="flex items-center space-x-2">
|
||||
<!-- Crear nueva carpeta -->
|
||||
<button wire:click="$emit('openCreateFolderModal')"
|
||||
class="px-3 py-2 text-sm text-white bg-blue-600 rounded-lg hover:bg-blue-700">
|
||||
<x-icons icon="folder-add" class="w-5 h-5 mr-1 inline" />
|
||||
Nueva Carpeta
|
||||
</button>
|
||||
|
||||
<!-- Subir archivos -->
|
||||
<div x-data="{ isUploading: false }"
|
||||
x-on:livewire-upload-start="isUploading = true"
|
||||
x-on:livewire-upload-finish="isUploading = false"
|
||||
x-on:livewire-upload-error="isUploading = false">
|
||||
<button @click="document.getElementById('file-input').click()"
|
||||
class="px-3 py-2 text-sm text-white bg-green-600 rounded-lg hover:bg-green-700"
|
||||
:disabled="isUploading">
|
||||
<x-icons icon="upload" class="w-5 h-5 mr-1 inline" />
|
||||
<span x-text="isUploading ? 'Subiendo...' : 'Subir Archivos'"></span>
|
||||
</button>
|
||||
<input type="file"
|
||||
id="file-input"
|
||||
wire:model="files"
|
||||
multiple
|
||||
class="hidden"
|
||||
@change="$wire.uploadFiles()">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Grupo derecho: Acciones adicionales -->
|
||||
<div class="flex items-center space-x-2">
|
||||
<!-- Menú desplegable con más opciones -->
|
||||
<div class="relative" x-data="{ open: false }">
|
||||
<button @click="open = !open"
|
||||
class="p-2 text-gray-600 hover:bg-gray-100 rounded-lg">
|
||||
<x-icons icon="dots-vertical" class="w-5 h-5" />
|
||||
</button>
|
||||
|
||||
<div x-show="open"
|
||||
@click.away="open = false"
|
||||
class="absolute right-0 z-10 w-48 bg-white rounded-lg shadow-lg">
|
||||
<div class="p-2 space-y-1">
|
||||
<button wire:click="$emit('openCreateSubfolderModal')"
|
||||
class="flex items-center w-full px-2 py-1 text-sm hover:bg-gray-100">
|
||||
<x-icons icon="folder-plus" class="w-4 h-4 mr-2" />
|
||||
Nueva Subcarpeta
|
||||
</button>
|
||||
<button wire:click="$emit('openMultiUploadModal')"
|
||||
class="flex items-center w-full px-2 py-1 text-sm hover:bg-gray-100">
|
||||
<x-icons icon="upload-multiple" class="w-4 h-4 mr-2" />
|
||||
Subida Masiva
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal para crear carpeta -->
|
||||
@livewire('folder.create-modal')
|
||||
176
resources/views/dashboard.blade.php
Normal file
176
resources/views/dashboard.blade.php
Normal file
@@ -0,0 +1,176 @@
|
||||
<!-- resources/views/layouts/app.blade.php -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
|
||||
<head>
|
||||
<!-- Cabecera estándar de Laravel -->
|
||||
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
||||
</head>
|
||||
<body class="bg-gray-100">
|
||||
<div x-data="{ sidebarOpen: false }" class="flex h-screen">
|
||||
<!-- Sidebar -->
|
||||
<aside class="bg-gray-800 text-white w-64 flex-shrink-0">
|
||||
<div class="p-4">
|
||||
<h1 class="text-2xl font-bold">mDMS</h1>
|
||||
</div>
|
||||
|
||||
<nav class="mt-6">
|
||||
<x-nav-link href="{{ route('dashboard') }}" :active="request()->routeIs('dashboard')">
|
||||
<x-icons icon="dashboard" class="w-5 h-5 mr-3" />
|
||||
Dashboard
|
||||
</x-nav-link>
|
||||
|
||||
<x-nav-link href="{{ route('projects.index') }}" :active="request()->routeIs('projects.*')">
|
||||
<x-icons icon="folder" class="w-5 h-5 mr-3" />
|
||||
Proyectos
|
||||
</x-nav-link>
|
||||
|
||||
<x-nav-link href="{{ route('documents.index') }}" :active="request()->routeIs('documents.*')">
|
||||
<x-icons icon="document" class="w-5 h-5 mr-3" />
|
||||
Documentos
|
||||
</x-nav-link>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<!-- Contenido principal -->
|
||||
<main class="flex-1 overflow-x-hidden overflow-y-auto">
|
||||
<!-- Header -->
|
||||
<header class="bg-white shadow">
|
||||
<div class="flex items-center justify-between px-6 py-4">
|
||||
<div class="flex-1 max-w-2xl">
|
||||
<div class="relative">
|
||||
<input type="search" placeholder="Buscar documentos..."
|
||||
class="w-full pl-10 pr-4 py-2 rounded-lg border focus:outline-none focus:border-blue-500">
|
||||
<x-icons icon="search" class="w-5 h-5 absolute left-3 top-2.5 text-gray-400" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center">
|
||||
<x-dropdown align="right">
|
||||
<x-slot name="trigger">
|
||||
<button class="flex items-center space-x-2">
|
||||
<span class="text-gray-700">{{ Auth::user()->name }}</span>
|
||||
<x-icons icon="chevron-down" class="w-4 h-4" />
|
||||
</button>
|
||||
</x-slot>
|
||||
|
||||
<x-dropdown-link href="{{ route('profile.show') }}">
|
||||
<x-icons icon="user" class="w-5 h-5 mr-2" /> Perfil
|
||||
</x-dropdown-link>
|
||||
<form method="POST" action="{{ route('logout') }}">
|
||||
@csrf
|
||||
<x-dropdown-link href="{{ route('logout') }}"
|
||||
onclick="event.preventDefault(); this.closest('form').submit();">
|
||||
<x-icons icon="logout" class="w-5 h-5 mr-2" /> Cerrar sesión
|
||||
</x-dropdown-link>
|
||||
</form>
|
||||
</x-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Contenido del dashboard -->
|
||||
<div class="p-6">
|
||||
<!-- Estadísticas rápidas -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
||||
<x-stats-card title="Proyectos" value=" {{ $stats['projects_count'] }} " icon="folder" color="bg-blue-500"/>
|
||||
<x-stats-card title="Documentos" value=" {{ $stats['documents_count'] }} " icon="document" color="bg-green-500" />
|
||||
<x-stats-card title="Almacenamiento" value=" {{$stats['users_count'] }} " icon="storage" color="bg-purple-500" />
|
||||
<x-stats-card title="Pendientes" value=" {{ $stats['storage_used'] }} " icon="clock" color="bg-yellow-500" />
|
||||
</div>
|
||||
|
||||
<!-- Documentos recientes -->
|
||||
<div class="bg-white rounded-lg shadow mb-8">
|
||||
<div class="px-6 py-4 border-b">
|
||||
<h3 class="text-lg font-semibold">Documentos recientes</h3>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Nombre</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Tipo</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Estado</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200">
|
||||
@foreach($recentDocuments as $document)
|
||||
<tr>
|
||||
<td class="px-6 py-4">{{ $document->name }}</td>
|
||||
<td class="px-6 py-4">
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
|
||||
{{ $document->type }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<x-status-badge :status="$document->status" />
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<x-dropdown>
|
||||
<x-dropdown-link href="{{ route('documents.show', $document) }}">
|
||||
<x-icons icon="eye" class="w-4 h-4 mr-2" /> Ver
|
||||
</x-dropdown-link>
|
||||
<x-dropdown-link href="#">
|
||||
<x-icons icon="download" class="w-4 h-4 mr-2" /> Descargar
|
||||
</x-dropdown-link>
|
||||
</x-dropdown>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actividad reciente -->
|
||||
<div class="grid lg:grid-cols-2 gap-6">
|
||||
<div class="bg-white rounded-lg shadow">
|
||||
<div class="px-6 py-4 border-b">
|
||||
<h3 class="text-lg font-semibold">Actividad reciente</h3>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<div class="flow-root">
|
||||
<ul class="-mb-4">
|
||||
@foreach($recentActivities as $activity)
|
||||
<li class="mb-4">
|
||||
<div class="flex items-center space-x-3">
|
||||
<div class="flex-shrink-0">
|
||||
<x-activity-icon :type="$activity->type" />
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<p class="text-sm text-gray-900">{{ $activity->description }}</p>
|
||||
<p class="text-xs text-gray-500">{{ $activity->created_at->diffForHumans() }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Uso de almacenamiento -->
|
||||
<div class="bg-white rounded-lg shadow">
|
||||
<div class="px-6 py-4 border-b">
|
||||
<h3 class="text-lg font-semibold">Uso de almacenamiento</h3>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<p class="text-2xl font-bold">{{ $stats['storage_used'] }}</p>
|
||||
<p class="text-sm text-gray-500">de {{ $stats['storage_limit'] }} disponibles</p>
|
||||
</div>
|
||||
<x-icons icon="storage" class="w-12 h-12 text-purple-500" />
|
||||
</div>
|
||||
<div class="w-full bg-gray-200 rounded-full h-2">
|
||||
div class="bg-purple-500 rounded-full h-2"
|
||||
style="width: {{ $stats['storage_percentage'] }}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
18
resources/views/dashboard.blade.php_old
Normal file
18
resources/views/dashboard.blade.php_old
Normal file
@@ -0,0 +1,18 @@
|
||||
<x-layouts.app :title="__('Dashboard')">
|
||||
<div class="flex h-full w-full flex-1 flex-col gap-4 rounded-xl">
|
||||
<div class="grid auto-rows-min gap-4 md:grid-cols-3">
|
||||
<div class="relative aspect-video overflow-hidden rounded-xl border border-neutral-200 dark:border-neutral-700">
|
||||
<x-placeholder-pattern class="absolute inset-0 size-full stroke-gray-900/20 dark:stroke-neutral-100/20" />
|
||||
</div>
|
||||
<div class="relative aspect-video overflow-hidden rounded-xl border border-neutral-200 dark:border-neutral-700">
|
||||
<x-placeholder-pattern class="absolute inset-0 size-full stroke-gray-900/20 dark:stroke-neutral-100/20" />
|
||||
</div>
|
||||
<div class="relative aspect-video overflow-hidden rounded-xl border border-neutral-200 dark:border-neutral-700">
|
||||
<x-placeholder-pattern class="absolute inset-0 size-full stroke-gray-900/20 dark:stroke-neutral-100/20" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative h-full flex-1 overflow-hidden rounded-xl border border-neutral-200 dark:border-neutral-700">
|
||||
<x-placeholder-pattern class="absolute inset-0 size-full stroke-gray-900/20 dark:stroke-neutral-100/20" />
|
||||
</div>
|
||||
</div>
|
||||
</x-layouts.app>
|
||||
47
resources/views/flux/icon/book-open-text.blade.php
Normal file
47
resources/views/flux/icon/book-open-text.blade.php
Normal file
@@ -0,0 +1,47 @@
|
||||
{{-- Credit: Lucide (https://lucide.dev) --}}
|
||||
|
||||
@props([
|
||||
'variant' => 'outline',
|
||||
])
|
||||
|
||||
@php
|
||||
if ($variant === 'solid') {
|
||||
throw new \Exception('The "solid" variant is not supported in Lucide.');
|
||||
}
|
||||
|
||||
$classes = Flux::classes('shrink-0')->add(
|
||||
match ($variant) {
|
||||
'outline' => '[:where(&)]:size-6',
|
||||
'solid' => '[:where(&)]:size-6',
|
||||
'mini' => '[:where(&)]:size-5',
|
||||
'micro' => '[:where(&)]:size-4',
|
||||
},
|
||||
);
|
||||
|
||||
$strokeWidth = match ($variant) {
|
||||
'outline' => 2,
|
||||
'mini' => 2.25,
|
||||
'micro' => 2.5,
|
||||
};
|
||||
@endphp
|
||||
|
||||
<svg
|
||||
{{ $attributes->class($classes) }}
|
||||
data-flux-icon
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="{{ $strokeWidth }}"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
aria-hidden="true"
|
||||
data-slot="icon"
|
||||
>
|
||||
<path d="M12 7v14" />
|
||||
<path d="M16 12h2" />
|
||||
<path d="M16 8h2" />
|
||||
<path d="M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z" />
|
||||
<path d="M6 12h2" />
|
||||
<path d="M6 8h2" />
|
||||
</svg>
|
||||
43
resources/views/flux/icon/chevrons-up-down.blade.php
Normal file
43
resources/views/flux/icon/chevrons-up-down.blade.php
Normal file
@@ -0,0 +1,43 @@
|
||||
{{-- Credit: Lucide (https://lucide.dev) --}}
|
||||
|
||||
@props([
|
||||
'variant' => 'outline',
|
||||
])
|
||||
|
||||
@php
|
||||
if ($variant === 'solid') {
|
||||
throw new \Exception('The "solid" variant is not supported in Lucide.');
|
||||
}
|
||||
|
||||
$classes = Flux::classes('shrink-0')->add(
|
||||
match ($variant) {
|
||||
'outline' => '[:where(&)]:size-6',
|
||||
'solid' => '[:where(&)]:size-6',
|
||||
'mini' => '[:where(&)]:size-5',
|
||||
'micro' => '[:where(&)]:size-4',
|
||||
},
|
||||
);
|
||||
|
||||
$strokeWidth = match ($variant) {
|
||||
'outline' => 2,
|
||||
'mini' => 2.25,
|
||||
'micro' => 2.5,
|
||||
};
|
||||
@endphp
|
||||
|
||||
<svg
|
||||
{{ $attributes->class($classes) }}
|
||||
data-flux-icon
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="{{ $strokeWidth }}"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
aria-hidden="true"
|
||||
data-slot="icon"
|
||||
>
|
||||
<path d="m7 15 5 5 5-5" />
|
||||
<path d="m7 9 5-5 5 5" />
|
||||
</svg>
|
||||
45
resources/views/flux/icon/folder-git-2.blade.php
Normal file
45
resources/views/flux/icon/folder-git-2.blade.php
Normal file
@@ -0,0 +1,45 @@
|
||||
{{-- Credit: Lucide (https://lucide.dev) --}}
|
||||
|
||||
@props([
|
||||
'variant' => 'outline',
|
||||
])
|
||||
|
||||
@php
|
||||
if ($variant === 'solid') {
|
||||
throw new \Exception('The "solid" variant is not supported in Lucide.');
|
||||
}
|
||||
|
||||
$classes = Flux::classes('shrink-0')->add(
|
||||
match ($variant) {
|
||||
'outline' => '[:where(&)]:size-6',
|
||||
'solid' => '[:where(&)]:size-6',
|
||||
'mini' => '[:where(&)]:size-5',
|
||||
'micro' => '[:where(&)]:size-4',
|
||||
},
|
||||
);
|
||||
|
||||
$strokeWidth = match ($variant) {
|
||||
'outline' => 2,
|
||||
'mini' => 2.25,
|
||||
'micro' => 2.5,
|
||||
};
|
||||
@endphp
|
||||
|
||||
<svg
|
||||
{{ $attributes->class($classes) }}
|
||||
data-flux-icon
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="{{ $strokeWidth }}"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
aria-hidden="true"
|
||||
data-slot="icon"
|
||||
>
|
||||
<path d="M9 20H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H20a2 2 0 0 1 2 2v5" />
|
||||
<circle cx="13" cy="12" r="2" />
|
||||
<path d="M18 19c-2.8 0-5-2.2-5-5v8" />
|
||||
<circle cx="20" cy="19" r="2" />
|
||||
</svg>
|
||||
45
resources/views/flux/icon/layout-grid.blade.php
Normal file
45
resources/views/flux/icon/layout-grid.blade.php
Normal file
@@ -0,0 +1,45 @@
|
||||
{{-- Credit: Lucide (https://lucide.dev) --}}
|
||||
|
||||
@props([
|
||||
'variant' => 'outline',
|
||||
])
|
||||
|
||||
@php
|
||||
if ($variant === 'solid') {
|
||||
throw new \Exception('The "solid" variant is not supported in Lucide.');
|
||||
}
|
||||
|
||||
$classes = Flux::classes('shrink-0')->add(
|
||||
match ($variant) {
|
||||
'outline' => '[:where(&)]:size-6',
|
||||
'solid' => '[:where(&)]:size-6',
|
||||
'mini' => '[:where(&)]:size-5',
|
||||
'micro' => '[:where(&)]:size-4',
|
||||
},
|
||||
);
|
||||
|
||||
$strokeWidth = match ($variant) {
|
||||
'outline' => 2,
|
||||
'mini' => 2.25,
|
||||
'micro' => 2.5,
|
||||
};
|
||||
@endphp
|
||||
|
||||
<svg
|
||||
{{ $attributes->class($classes) }}
|
||||
data-flux-icon
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="{{ $strokeWidth }}"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
aria-hidden="true"
|
||||
data-slot="icon"
|
||||
>
|
||||
<rect width="7" height="7" x="3" y="3" rx="1" />
|
||||
<rect width="7" height="7" x="14" y="3" rx="1" />
|
||||
<rect width="7" height="7" x="14" y="14" rx="1" />
|
||||
<rect width="7" height="7" x="3" y="14" rx="1" />
|
||||
</svg>
|
||||
51
resources/views/flux/navlist/group.blade.php
Normal file
51
resources/views/flux/navlist/group.blade.php
Normal file
@@ -0,0 +1,51 @@
|
||||
@props([
|
||||
'expandable' => false,
|
||||
'expanded' => true,
|
||||
'heading' => null,
|
||||
])
|
||||
|
||||
<?php if ($expandable && $heading): ?>
|
||||
|
||||
<ui-disclosure
|
||||
{{ $attributes->class('group/disclosure') }}
|
||||
@if ($expanded === true) open @endif
|
||||
data-flux-navlist-group
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="group/disclosure-button mb-[2px] flex h-10 w-full items-center rounded-lg text-zinc-500 hover:bg-zinc-800/5 hover:text-zinc-800 lg:h-8 dark:text-white/80 dark:hover:bg-white/[7%] dark:hover:text-white"
|
||||
>
|
||||
<div class="ps-3 pe-4">
|
||||
<flux:icon.chevron-down class="hidden size-3! group-data-open/disclosure-button:block" />
|
||||
<flux:icon.chevron-right class="block size-3! group-data-open/disclosure-button:hidden" />
|
||||
</div>
|
||||
|
||||
<span class="text-sm font-medium leading-none">{{ $heading }}</span>
|
||||
</button>
|
||||
|
||||
<div class="relative hidden space-y-[2px] ps-7 data-open:block" @if ($expanded === true) data-open @endif>
|
||||
<div class="absolute inset-y-[3px] start-0 ms-4 w-px bg-zinc-200 dark:bg-white/30"></div>
|
||||
|
||||
{{ $slot }}
|
||||
</div>
|
||||
</ui-disclosure>
|
||||
|
||||
<?php elseif ($heading): ?>
|
||||
|
||||
<div {{ $attributes->class('block space-y-[2px]') }}>
|
||||
<div class="px-1 py-2">
|
||||
<div class="text-xs leading-none text-zinc-400">{{ $heading }}</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{{ $slot }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php else: ?>
|
||||
|
||||
<div {{ $attributes->class('block space-y-[2px]') }}>
|
||||
{{ $slot }}
|
||||
</div>
|
||||
|
||||
<?php endif; ?>
|
||||
3
resources/views/livewire/approval-workflow.blade.php
Normal file
3
resources/views/livewire/approval-workflow.blade.php
Normal file
@@ -0,0 +1,3 @@
|
||||
<div>
|
||||
{{-- The best athlete wants his opponent at his best. --}}
|
||||
</div>
|
||||
57
resources/views/livewire/auth/confirm-password.blade.php
Normal file
57
resources/views/livewire/auth/confirm-password.blade.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Livewire\Attributes\Layout;
|
||||
use Livewire\Volt\Component;
|
||||
|
||||
new #[Layout('components.layouts.auth')] class extends Component {
|
||||
public string $password = '';
|
||||
|
||||
/**
|
||||
* Confirm the current user's password.
|
||||
*/
|
||||
public function confirmPassword(): void
|
||||
{
|
||||
$this->validate([
|
||||
'password' => ['required', 'string'],
|
||||
]);
|
||||
|
||||
if (! Auth::guard('web')->validate([
|
||||
'email' => Auth::user()->email,
|
||||
'password' => $this->password,
|
||||
])) {
|
||||
throw ValidationException::withMessages([
|
||||
'password' => __('auth.password'),
|
||||
]);
|
||||
}
|
||||
|
||||
session(['auth.password_confirmed_at' => time()]);
|
||||
|
||||
$this->redirectIntended(default: route('dashboard', absolute: false), navigate: true);
|
||||
}
|
||||
}; ?>
|
||||
|
||||
<div class="flex flex-col gap-6">
|
||||
<x-auth-header
|
||||
:title="__('Confirm password')"
|
||||
:description="__('This is a secure area of the application. Please confirm your password before continuing.')"
|
||||
/>
|
||||
|
||||
<!-- Session Status -->
|
||||
<x-auth-session-status class="text-center" :status="session('status')" />
|
||||
|
||||
<form wire:submit="confirmPassword" class="flex flex-col gap-6">
|
||||
<!-- Password -->
|
||||
<flux:input
|
||||
wire:model="password"
|
||||
:label="__('Password')"
|
||||
type="password"
|
||||
required
|
||||
autocomplete="new-password"
|
||||
:placeholder="__('Password')"
|
||||
/>
|
||||
|
||||
<flux:button variant="primary" type="submit" class="w-full">{{ __('Confirm') }}</flux:button>
|
||||
</form>
|
||||
</div>
|
||||
49
resources/views/livewire/auth/forgot-password.blade.php
Normal file
49
resources/views/livewire/auth/forgot-password.blade.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Password;
|
||||
use Livewire\Attributes\Layout;
|
||||
use Livewire\Volt\Component;
|
||||
|
||||
new #[Layout('components.layouts.auth')] class extends Component {
|
||||
public string $email = '';
|
||||
|
||||
/**
|
||||
* Send a password reset link to the provided email address.
|
||||
*/
|
||||
public function sendPasswordResetLink(): void
|
||||
{
|
||||
$this->validate([
|
||||
'email' => ['required', 'string', 'email'],
|
||||
]);
|
||||
|
||||
Password::sendResetLink($this->only('email'));
|
||||
|
||||
session()->flash('status', __('A reset link will be sent if the account exists.'));
|
||||
}
|
||||
}; ?>
|
||||
|
||||
<div class="flex flex-col gap-6">
|
||||
<x-auth-header :title="__('Forgot password')" :description="__('Enter your email to receive a password reset link')" />
|
||||
|
||||
<!-- Session Status -->
|
||||
<x-auth-session-status class="text-center" :status="session('status')" />
|
||||
|
||||
<form wire:submit="sendPasswordResetLink" class="flex flex-col gap-6">
|
||||
<!-- Email Address -->
|
||||
<flux:input
|
||||
wire:model="email"
|
||||
:label="__('Email Address')"
|
||||
type="email"
|
||||
required
|
||||
autofocus
|
||||
placeholder="email@example.com"
|
||||
/>
|
||||
|
||||
<flux:button variant="primary" type="submit" class="w-full">{{ __('Email password reset link') }}</flux:button>
|
||||
</form>
|
||||
|
||||
<div class="space-x-1 rtl:space-x-reverse text-center text-sm text-zinc-400">
|
||||
{{ __('Or, return to') }}
|
||||
<flux:link :href="route('login')" wire:navigate>{{ __('log in') }}</flux:link>
|
||||
</div>
|
||||
</div>
|
||||
126
resources/views/livewire/auth/login.blade.php
Normal file
126
resources/views/livewire/auth/login.blade.php
Normal file
@@ -0,0 +1,126 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Auth\Events\Lockout;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Illuminate\Support\Facades\Session;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Livewire\Attributes\Layout;
|
||||
use Livewire\Attributes\Validate;
|
||||
use Livewire\Volt\Component;
|
||||
|
||||
new #[Layout('components.layouts.auth')] class extends Component {
|
||||
#[Validate('required|string|email')]
|
||||
public string $email = '';
|
||||
|
||||
#[Validate('required|string')]
|
||||
public string $password = '';
|
||||
|
||||
public bool $remember = false;
|
||||
|
||||
/**
|
||||
* Handle an incoming authentication request.
|
||||
*/
|
||||
public function login(): void
|
||||
{
|
||||
$this->validate();
|
||||
|
||||
$this->ensureIsNotRateLimited();
|
||||
|
||||
if (! Auth::attempt(['email' => $this->email, 'password' => $this->password], $this->remember)) {
|
||||
RateLimiter::hit($this->throttleKey());
|
||||
|
||||
throw ValidationException::withMessages([
|
||||
'email' => __('auth.failed'),
|
||||
]);
|
||||
}
|
||||
|
||||
RateLimiter::clear($this->throttleKey());
|
||||
Session::regenerate();
|
||||
|
||||
$this->redirectIntended(default: route('dashboard', absolute: false), navigate: true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the authentication request is not rate limited.
|
||||
*/
|
||||
protected function ensureIsNotRateLimited(): void
|
||||
{
|
||||
if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) {
|
||||
return;
|
||||
}
|
||||
|
||||
event(new Lockout(request()));
|
||||
|
||||
$seconds = RateLimiter::availableIn($this->throttleKey());
|
||||
|
||||
throw ValidationException::withMessages([
|
||||
'email' => __('auth.throttle', [
|
||||
'seconds' => $seconds,
|
||||
'minutes' => ceil($seconds / 60),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the authentication rate limiting throttle key.
|
||||
*/
|
||||
protected function throttleKey(): string
|
||||
{
|
||||
return Str::transliterate(Str::lower($this->email).'|'.request()->ip());
|
||||
}
|
||||
}; ?>
|
||||
|
||||
<div class="flex flex-col gap-6">
|
||||
<x-auth-header :title="__('Log in to your account')" :description="__('Enter your email and password below to log in')" />
|
||||
|
||||
<!-- Session Status -->
|
||||
<x-auth-session-status class="text-center" :status="session('status')" />
|
||||
|
||||
<form wire:submit="login" class="flex flex-col gap-6">
|
||||
<!-- Email Address -->
|
||||
<flux:input
|
||||
wire:model="email"
|
||||
:label="__('Email address')"
|
||||
type="email"
|
||||
required
|
||||
autofocus
|
||||
autocomplete="email"
|
||||
placeholder="email@example.com"
|
||||
/>
|
||||
|
||||
<!-- Password -->
|
||||
<div class="relative">
|
||||
<flux:input
|
||||
wire:model="password"
|
||||
:label="__('Password')"
|
||||
type="password"
|
||||
required
|
||||
autocomplete="current-password"
|
||||
:placeholder="__('Password')"
|
||||
/>
|
||||
|
||||
@if (Route::has('password.request'))
|
||||
<flux:link class="absolute end-0 top-0 text-sm" :href="route('password.request')" wire:navigate>
|
||||
{{ __('Forgot your password?') }}
|
||||
</flux:link>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- Remember Me -->
|
||||
<flux:checkbox wire:model="remember" :label="__('Remember me')" />
|
||||
|
||||
<div class="flex items-center justify-end">
|
||||
<flux:button variant="primary" type="submit" class="w-full">{{ __('Log in') }}</flux:button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@if (Route::has('register'))
|
||||
<div class="space-x-1 rtl:space-x-reverse text-center text-sm text-zinc-600 dark:text-zinc-400">
|
||||
{{ __('Don\'t have an account?') }}
|
||||
<flux:link :href="route('register')" wire:navigate>{{ __('Sign up') }}</flux:link>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
97
resources/views/livewire/auth/register.blade.php
Normal file
97
resources/views/livewire/auth/register.blade.php
Normal file
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Auth\Events\Registered;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Validation\Rules;
|
||||
use Livewire\Attributes\Layout;
|
||||
use Livewire\Volt\Component;
|
||||
|
||||
new #[Layout('components.layouts.auth')] class extends Component {
|
||||
public string $name = '';
|
||||
public string $email = '';
|
||||
public string $password = '';
|
||||
public string $password_confirmation = '';
|
||||
|
||||
/**
|
||||
* Handle an incoming registration request.
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
$validated = $this->validate([
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:' . User::class],
|
||||
'password' => ['required', 'string', 'confirmed', Rules\Password::defaults()],
|
||||
]);
|
||||
|
||||
$validated['password'] = Hash::make($validated['password']);
|
||||
|
||||
event(new Registered(($user = User::create($validated))));
|
||||
|
||||
Auth::login($user);
|
||||
|
||||
$this->redirectIntended(route('dashboard', absolute: false), navigate: true);
|
||||
}
|
||||
}; ?>
|
||||
|
||||
<div class="flex flex-col gap-6">
|
||||
<x-auth-header :title="__('Create an account')" :description="__('Enter your details below to create your account')" />
|
||||
|
||||
<!-- Session Status -->
|
||||
<x-auth-session-status class="text-center" :status="session('status')" />
|
||||
|
||||
<form wire:submit="register" class="flex flex-col gap-6">
|
||||
<!-- Name -->
|
||||
<flux:input
|
||||
wire:model="name"
|
||||
:label="__('Name')"
|
||||
type="text"
|
||||
required
|
||||
autofocus
|
||||
autocomplete="name"
|
||||
:placeholder="__('Full name')"
|
||||
/>
|
||||
|
||||
<!-- Email Address -->
|
||||
<flux:input
|
||||
wire:model="email"
|
||||
:label="__('Email address')"
|
||||
type="email"
|
||||
required
|
||||
autocomplete="email"
|
||||
placeholder="email@example.com"
|
||||
/>
|
||||
|
||||
<!-- Password -->
|
||||
<flux:input
|
||||
wire:model="password"
|
||||
:label="__('Password')"
|
||||
type="password"
|
||||
required
|
||||
autocomplete="new-password"
|
||||
:placeholder="__('Password')"
|
||||
/>
|
||||
|
||||
<!-- Confirm Password -->
|
||||
<flux:input
|
||||
wire:model="password_confirmation"
|
||||
:label="__('Confirm password')"
|
||||
type="password"
|
||||
required
|
||||
autocomplete="new-password"
|
||||
:placeholder="__('Confirm password')"
|
||||
/>
|
||||
|
||||
<div class="flex items-center justify-end">
|
||||
<flux:button type="submit" variant="primary" class="w-full">
|
||||
{{ __('Create account') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="space-x-1 rtl:space-x-reverse text-center text-sm text-zinc-600 dark:text-zinc-400">
|
||||
{{ __('Already have an account?') }}
|
||||
<flux:link :href="route('login')" wire:navigate>{{ __('Log in') }}</flux:link>
|
||||
</div>
|
||||
</div>
|
||||
113
resources/views/livewire/auth/reset-password.blade.php
Normal file
113
resources/views/livewire/auth/reset-password.blade.php
Normal file
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Auth\Events\PasswordReset;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Password;
|
||||
use Illuminate\Support\Facades\Session;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\Rules;
|
||||
use Livewire\Attributes\Layout;
|
||||
use Livewire\Attributes\Locked;
|
||||
use Livewire\Volt\Component;
|
||||
|
||||
new #[Layout('components.layouts.auth')] class extends Component {
|
||||
#[Locked]
|
||||
public string $token = '';
|
||||
public string $email = '';
|
||||
public string $password = '';
|
||||
public string $password_confirmation = '';
|
||||
|
||||
/**
|
||||
* Mount the component.
|
||||
*/
|
||||
public function mount(string $token): void
|
||||
{
|
||||
$this->token = $token;
|
||||
|
||||
$this->email = request()->string('email');
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the password for the given user.
|
||||
*/
|
||||
public function resetPassword(): void
|
||||
{
|
||||
$this->validate([
|
||||
'token' => ['required'],
|
||||
'email' => ['required', 'string', 'email'],
|
||||
'password' => ['required', 'string', 'confirmed', Rules\Password::defaults()],
|
||||
]);
|
||||
|
||||
// Here we will attempt to reset the user's password. If it is successful we
|
||||
// will update the password on an actual user model and persist it to the
|
||||
// database. Otherwise we will parse the error and return the response.
|
||||
$status = Password::reset(
|
||||
$this->only('email', 'password', 'password_confirmation', 'token'),
|
||||
function ($user) {
|
||||
$user->forceFill([
|
||||
'password' => Hash::make($this->password),
|
||||
'remember_token' => Str::random(60),
|
||||
])->save();
|
||||
|
||||
event(new PasswordReset($user));
|
||||
}
|
||||
);
|
||||
|
||||
// If the password was successfully reset, we will redirect the user back to
|
||||
// the application's home authenticated view. If there is an error we can
|
||||
// redirect them back to where they came from with their error message.
|
||||
if ($status != Password::PasswordReset) {
|
||||
$this->addError('email', __($status));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
Session::flash('status', __($status));
|
||||
|
||||
$this->redirectRoute('login', navigate: true);
|
||||
}
|
||||
}; ?>
|
||||
|
||||
<div class="flex flex-col gap-6">
|
||||
<x-auth-header :title="__('Reset password')" :description="__('Please enter your new password below')" />
|
||||
|
||||
<!-- Session Status -->
|
||||
<x-auth-session-status class="text-center" :status="session('status')" />
|
||||
|
||||
<form wire:submit="resetPassword" class="flex flex-col gap-6">
|
||||
<!-- Email Address -->
|
||||
<flux:input
|
||||
wire:model="email"
|
||||
:label="__('Email')"
|
||||
type="email"
|
||||
required
|
||||
autocomplete="email"
|
||||
/>
|
||||
|
||||
<!-- Password -->
|
||||
<flux:input
|
||||
wire:model="password"
|
||||
:label="__('Password')"
|
||||
type="password"
|
||||
required
|
||||
autocomplete="new-password"
|
||||
:placeholder="__('Password')"
|
||||
/>
|
||||
|
||||
<!-- Confirm Password -->
|
||||
<flux:input
|
||||
wire:model="password_confirmation"
|
||||
:label="__('Confirm password')"
|
||||
type="password"
|
||||
required
|
||||
autocomplete="new-password"
|
||||
:placeholder="__('Confirm password')"
|
||||
/>
|
||||
|
||||
<div class="flex items-center justify-end">
|
||||
<flux:button type="submit" variant="primary" class="w-full">
|
||||
{{ __('Reset password') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
57
resources/views/livewire/auth/verify-email.blade.php
Normal file
57
resources/views/livewire/auth/verify-email.blade.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
use App\Livewire\Actions\Logout;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Session;
|
||||
use Livewire\Attributes\Layout;
|
||||
use Livewire\Volt\Component;
|
||||
|
||||
new #[Layout('components.layouts.auth')] class extends Component {
|
||||
/**
|
||||
* Send an email verification notification to the user.
|
||||
*/
|
||||
public function sendVerification(): void
|
||||
{
|
||||
if (Auth::user()->hasVerifiedEmail()) {
|
||||
$this->redirectIntended(default: route('dashboard', absolute: false), navigate: true);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
Auth::user()->sendEmailVerificationNotification();
|
||||
|
||||
Session::flash('status', 'verification-link-sent');
|
||||
}
|
||||
|
||||
/**
|
||||
* Log the current user out of the application.
|
||||
*/
|
||||
public function logout(Logout $logout): void
|
||||
{
|
||||
$logout();
|
||||
|
||||
$this->redirect('/', navigate: true);
|
||||
}
|
||||
}; ?>
|
||||
|
||||
<div class="mt-4 flex flex-col gap-6">
|
||||
<flux:text class="text-center">
|
||||
{{ __('Please verify your email address by clicking on the link we just emailed to you.') }}
|
||||
</flux:text>
|
||||
|
||||
@if (session('status') == 'verification-link-sent')
|
||||
<flux:text class="text-center font-medium !dark:text-green-400 !text-green-600">
|
||||
{{ __('A new verification link has been sent to the email address you provided during registration.') }}
|
||||
</flux:text>
|
||||
@endif
|
||||
|
||||
<div class="flex flex-col items-center justify-between space-y-3">
|
||||
<flux:button wire:click="sendVerification" variant="primary" class="w-full">
|
||||
{{ __('Resend verification email') }}
|
||||
</flux:button>
|
||||
|
||||
<flux:link class="text-sm cursor-pointer" wire:click="logout">
|
||||
{{ __('Log out') }}
|
||||
</flux:link>
|
||||
</div>
|
||||
</div>
|
||||
3
resources/views/livewire/comment-system.blade.php
Normal file
3
resources/views/livewire/comment-system.blade.php
Normal file
@@ -0,0 +1,3 @@
|
||||
<div>
|
||||
{{-- Success is as dangerous as failure. --}}
|
||||
</div>
|
||||
3
resources/views/livewire/document-browser.blade.php
Normal file
3
resources/views/livewire/document-browser.blade.php
Normal file
@@ -0,0 +1,3 @@
|
||||
<div>
|
||||
{{-- Success is as dangerous as failure. --}}
|
||||
</div>
|
||||
26
resources/views/livewire/folder/create-modal.blade.php
Normal file
26
resources/views/livewire/folder/create-modal.blade.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<!-- resources/views/livewire/folder/create-modal.blade.php -->
|
||||
<div x-show="$wire.showModal"
|
||||
class="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50">
|
||||
<div class="w-full max-w-md p-6 bg-white rounded-lg">
|
||||
<h3 class="mb-4 text-lg font-semibold">
|
||||
{{ $parentFolder ? 'Nueva Subcarpeta en '.$parentFolder->name : 'Nueva Carpeta' }}
|
||||
</h3>
|
||||
|
||||
<input type="text"
|
||||
wire:model="folderName"
|
||||
placeholder="Nombre de la carpeta"
|
||||
class="w-full p-2 border rounded-lg"
|
||||
@keyup.enter="createFolder">
|
||||
|
||||
<div class="flex justify-end mt-4 space-x-2">
|
||||
<button @click="$wire.showModal = false"
|
||||
class="px-4 py-2 text-gray-600 hover:bg-gray-100 rounded-lg">
|
||||
Cancelar
|
||||
</button>
|
||||
<button wire:click="createFolder"
|
||||
class="px-4 py-2 text-white bg-blue-600 rounded-lg hover:bg-blue-700">
|
||||
Crear
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
84
resources/views/livewire/project-show.blade.php
Normal file
84
resources/views/livewire/project-show.blade.php
Normal file
@@ -0,0 +1,84 @@
|
||||
<div>
|
||||
<!-- Header del Proyecto -->
|
||||
<div class="flex items-center justify-between pb-6 border-b">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold">{{ $project->name }}</h1>
|
||||
<div class="flex items-center mt-2 space-x-4">
|
||||
<span class="px-2 py-1 text-sm rounded-full bg-blue-100 text-blue-800">
|
||||
{{ ucfirst($project->status) }}
|
||||
</span>
|
||||
<span class="text-sm text-gray-500">
|
||||
{{ $project->created_at->format('d/m/Y') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<a href="{{ route('projects.edit', $project) }}"
|
||||
class="px-4 py-2 text-white bg-blue-600 rounded-lg hover:bg-blue-700">
|
||||
Editar Proyecto
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Layout Principal -->
|
||||
<div class="grid grid-cols-1 gap-6 mt-6 lg:grid-cols-4">
|
||||
<!-- Treeview de Carpetas -->
|
||||
<div class="col-span-1 bg-white rounded-lg shadow">
|
||||
<div class="p-4 border-b">
|
||||
<h3 class="font-medium">Estructura de Carpetas</h3>
|
||||
</div>
|
||||
<div class="p-4">
|
||||
<ul class="space-y-2">
|
||||
@foreach($rootFolders as $folder)
|
||||
<x-folder-item
|
||||
:folder="$folder"
|
||||
:selectedFolderId="$selectedFolderId"
|
||||
:expandedFolders="$expandedFolders"
|
||||
wire:key="folder-{{ $folder->id }}"
|
||||
/>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Listado de Documentos -->
|
||||
<div class="col-span-3">
|
||||
<div class="bg-white rounded-lg shadow">
|
||||
<div class="p-4 border-b">
|
||||
<h3 class="font-medium">Documentos</h3>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">Nombre</th>
|
||||
<th class="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">Versiones</th>
|
||||
<th class="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">Tamaño</th>
|
||||
<th class="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">Estado</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
@foreach($this->documents as $document)
|
||||
<tr class="hover:bg-gray-50">
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="flex items-center">
|
||||
<x-icons icon="document" class="w-5 h-5 mr-2 text-gray-400" />
|
||||
{{ $document->name }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
{{ $document->versions->count() }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
{{ $document->human_readable_size }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<x-status-badge :status="$document->status" />
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
19
resources/views/livewire/settings/appearance.blade.php
Normal file
19
resources/views/livewire/settings/appearance.blade.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
use Livewire\Volt\Component;
|
||||
|
||||
new class extends Component {
|
||||
//
|
||||
}; ?>
|
||||
|
||||
<section class="w-full">
|
||||
@include('partials.settings-heading')
|
||||
|
||||
<x-settings.layout :heading="__('Appearance')" :subheading=" __('Update the appearance settings for your account')">
|
||||
<flux:radio.group x-data variant="segmented" x-model="$flux.appearance">
|
||||
<flux:radio value="light" icon="sun">{{ __('Light') }}</flux:radio>
|
||||
<flux:radio value="dark" icon="moon">{{ __('Dark') }}</flux:radio>
|
||||
<flux:radio value="system" icon="computer-desktop">{{ __('System') }}</flux:radio>
|
||||
</flux:radio.group>
|
||||
</x-settings.layout>
|
||||
</section>
|
||||
58
resources/views/livewire/settings/delete-user-form.blade.php
Normal file
58
resources/views/livewire/settings/delete-user-form.blade.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
use App\Livewire\Actions\Logout;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Livewire\Volt\Component;
|
||||
|
||||
new class extends Component {
|
||||
public string $password = '';
|
||||
|
||||
/**
|
||||
* Delete the currently authenticated user.
|
||||
*/
|
||||
public function deleteUser(Logout $logout): void
|
||||
{
|
||||
$this->validate([
|
||||
'password' => ['required', 'string', 'current_password'],
|
||||
]);
|
||||
|
||||
tap(Auth::user(), $logout(...))->delete();
|
||||
|
||||
$this->redirect('/', navigate: true);
|
||||
}
|
||||
}; ?>
|
||||
|
||||
<section class="mt-10 space-y-6">
|
||||
<div class="relative mb-5">
|
||||
<flux:heading>{{ __('Delete account') }}</flux:heading>
|
||||
<flux:subheading>{{ __('Delete your account and all of its resources') }}</flux:subheading>
|
||||
</div>
|
||||
|
||||
<flux:modal.trigger name="confirm-user-deletion">
|
||||
<flux:button variant="danger" x-data="" x-on:click.prevent="$dispatch('open-modal', 'confirm-user-deletion')">
|
||||
{{ __('Delete account') }}
|
||||
</flux:button>
|
||||
</flux:modal.trigger>
|
||||
|
||||
<flux:modal name="confirm-user-deletion" :show="$errors->isNotEmpty()" focusable class="max-w-lg">
|
||||
<form wire:submit="deleteUser" class="space-y-6">
|
||||
<div>
|
||||
<flux:heading size="lg">{{ __('Are you sure you want to delete your account?') }}</flux:heading>
|
||||
|
||||
<flux:subheading>
|
||||
{{ __('Once your account is deleted, all of its resources and data will be permanently deleted. Please enter your password to confirm you would like to permanently delete your account.') }}
|
||||
</flux:subheading>
|
||||
</div>
|
||||
|
||||
<flux:input wire:model="password" :label="__('Password')" type="password" />
|
||||
|
||||
<div class="flex justify-end space-x-2 rtl:space-x-reverse">
|
||||
<flux:modal.close>
|
||||
<flux:button variant="filled">{{ __('Cancel') }}</flux:button>
|
||||
</flux:modal.close>
|
||||
|
||||
<flux:button variant="danger" type="submit">{{ __('Delete account') }}</flux:button>
|
||||
</div>
|
||||
</form>
|
||||
</flux:modal>
|
||||
</section>
|
||||
78
resources/views/livewire/settings/password.blade.php
Normal file
78
resources/views/livewire/settings/password.blade.php
Normal file
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Validation\Rules\Password;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Livewire\Volt\Component;
|
||||
|
||||
new class extends Component {
|
||||
public string $current_password = '';
|
||||
public string $password = '';
|
||||
public string $password_confirmation = '';
|
||||
|
||||
/**
|
||||
* Update the password for the currently authenticated user.
|
||||
*/
|
||||
public function updatePassword(): void
|
||||
{
|
||||
try {
|
||||
$validated = $this->validate([
|
||||
'current_password' => ['required', 'string', 'current_password'],
|
||||
'password' => ['required', 'string', Password::defaults(), 'confirmed'],
|
||||
]);
|
||||
} catch (ValidationException $e) {
|
||||
$this->reset('current_password', 'password', 'password_confirmation');
|
||||
|
||||
throw $e;
|
||||
}
|
||||
|
||||
Auth::user()->update([
|
||||
'password' => Hash::make($validated['password']),
|
||||
]);
|
||||
|
||||
$this->reset('current_password', 'password', 'password_confirmation');
|
||||
|
||||
$this->dispatch('password-updated');
|
||||
}
|
||||
}; ?>
|
||||
|
||||
<section class="w-full">
|
||||
@include('partials.settings-heading')
|
||||
|
||||
<x-settings.layout :heading="__('Update password')" :subheading="__('Ensure your account is using a long, random password to stay secure')">
|
||||
<form wire:submit="updatePassword" class="mt-6 space-y-6">
|
||||
<flux:input
|
||||
wire:model="current_password"
|
||||
:label="__('Current password')"
|
||||
type="password"
|
||||
required
|
||||
autocomplete="current-password"
|
||||
/>
|
||||
<flux:input
|
||||
wire:model="password"
|
||||
:label="__('New password')"
|
||||
type="password"
|
||||
required
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
<flux:input
|
||||
wire:model="password_confirmation"
|
||||
:label="__('Confirm Password')"
|
||||
type="password"
|
||||
required
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="flex items-center justify-end">
|
||||
<flux:button variant="primary" type="submit" class="w-full">{{ __('Save') }}</flux:button>
|
||||
</div>
|
||||
|
||||
<x-action-message class="me-3" on="password-updated">
|
||||
{{ __('Saved.') }}
|
||||
</x-action-message>
|
||||
</div>
|
||||
</form>
|
||||
</x-settings.layout>
|
||||
</section>
|
||||
114
resources/views/livewire/settings/profile.blade.php
Normal file
114
resources/views/livewire/settings/profile.blade.php
Normal file
@@ -0,0 +1,114 @@
|
||||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Session;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Livewire\Volt\Component;
|
||||
|
||||
new class extends Component {
|
||||
public string $name = '';
|
||||
public string $email = '';
|
||||
|
||||
/**
|
||||
* Mount the component.
|
||||
*/
|
||||
public function mount(): void
|
||||
{
|
||||
$this->name = Auth::user()->name;
|
||||
$this->email = Auth::user()->email;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the profile information for the currently authenticated user.
|
||||
*/
|
||||
public function updateProfileInformation(): void
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
||||
$validated = $this->validate([
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
|
||||
'email' => [
|
||||
'required',
|
||||
'string',
|
||||
'lowercase',
|
||||
'email',
|
||||
'max:255',
|
||||
Rule::unique(User::class)->ignore($user->id)
|
||||
],
|
||||
]);
|
||||
|
||||
$user->fill($validated);
|
||||
|
||||
if ($user->isDirty('email')) {
|
||||
$user->email_verified_at = null;
|
||||
}
|
||||
|
||||
$user->save();
|
||||
|
||||
$this->dispatch('profile-updated', name: $user->name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an email verification notification to the current user.
|
||||
*/
|
||||
public function resendVerificationNotification(): void
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
||||
if ($user->hasVerifiedEmail()) {
|
||||
$this->redirectIntended(default: route('dashboard', absolute: false));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$user->sendEmailVerificationNotification();
|
||||
|
||||
Session::flash('status', 'verification-link-sent');
|
||||
}
|
||||
}; ?>
|
||||
|
||||
<section class="w-full">
|
||||
@include('partials.settings-heading')
|
||||
|
||||
<x-settings.layout :heading="__('Profile')" :subheading="__('Update your name and email address')">
|
||||
<form wire:submit="updateProfileInformation" class="my-6 w-full space-y-6">
|
||||
<flux:input wire:model="name" :label="__('Name')" type="text" required autofocus autocomplete="name" />
|
||||
|
||||
<div>
|
||||
<flux:input wire:model="email" :label="__('Email')" type="email" required autocomplete="email" />
|
||||
|
||||
@if (auth()->user() instanceof \Illuminate\Contracts\Auth\MustVerifyEmail &&! auth()->user()->hasVerifiedEmail())
|
||||
<div>
|
||||
<flux:text class="mt-4">
|
||||
{{ __('Your email address is unverified.') }}
|
||||
|
||||
<flux:link class="text-sm cursor-pointer" wire:click.prevent="resendVerificationNotification">
|
||||
{{ __('Click here to re-send the verification email.') }}
|
||||
</flux:link>
|
||||
</flux:text>
|
||||
|
||||
@if (session('status') === 'verification-link-sent')
|
||||
<flux:text class="mt-2 font-medium !dark:text-green-400 !text-green-600">
|
||||
{{ __('A new verification link has been sent to your email address.') }}
|
||||
</flux:text>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="flex items-center justify-end">
|
||||
<flux:button variant="primary" type="submit" class="w-full">{{ __('Save') }}</flux:button>
|
||||
</div>
|
||||
|
||||
<x-action-message class="me-3" on="profile-updated">
|
||||
{{ __('Saved.') }}
|
||||
</x-action-message>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<livewire:settings.delete-user-form />
|
||||
</x-settings.layout>
|
||||
</section>
|
||||
10
resources/views/partials/head.blade.php
Normal file
10
resources/views/partials/head.blade.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
|
||||
<title>{{ $title ?? config('app.name') }}</title>
|
||||
|
||||
<link rel="preconnect" href="https://fonts.bunny.net">
|
||||
<link href="https://fonts.bunny.net/css?family=instrument-sans:400,500,600" rel="stylesheet" />
|
||||
|
||||
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
||||
@fluxAppearance
|
||||
5
resources/views/partials/settings-heading.blade.php
Normal file
5
resources/views/partials/settings-heading.blade.php
Normal file
@@ -0,0 +1,5 @@
|
||||
<div class="relative mb-6 w-full">
|
||||
<flux:heading size="xl" level="1">{{ __('Settings') }}</flux:heading>
|
||||
<flux:subheading size="lg" class="mb-6">{{ __('Manage your profile and account settings') }}</flux:subheading>
|
||||
<flux:separator variant="subtle" />
|
||||
</div>
|
||||
157
resources/views/permissions/index.blade.php
Normal file
157
resources/views/permissions/index.blade.php
Normal file
@@ -0,0 +1,157 @@
|
||||
<!-- resources/views/permissions/assign.blade.php -->
|
||||
x-layouts.app :title="__('Permissions Management')">
|
||||
<x-slot name="header">
|
||||
<h2 class="text-xl font-semibold leading-tight text-gray-800">
|
||||
{{ __('Gestión de Permisos y Roles') }}
|
||||
</h2>
|
||||
</x-slot>
|
||||
|
||||
<div class="py-6">
|
||||
<div class="mx-auto max-w-7xl sm:px-6 lg:px-8">
|
||||
<div class="overflow-hidden bg-white shadow-sm sm:rounded-lg">
|
||||
<div class="p-6 bg-white border-b border-gray-200">
|
||||
<!-- Buscador -->
|
||||
<div class="mb-6">
|
||||
<input type="text" id="search" placeholder="Buscar usuario..."
|
||||
class="w-full px-4 py-2 border rounded-lg focus:ring-blue-500 focus:border-blue-500">
|
||||
</div>
|
||||
|
||||
<!-- Lista de Usuarios -->
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">Usuario</th>
|
||||
<th class="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">Roles</th>
|
||||
<th class="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">Permisos Directos</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200" id="users-table">
|
||||
@foreach($users as $user)
|
||||
<tr class="user-row">
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="flex items-center">
|
||||
<div class="ml-4">
|
||||
<div class="text-sm font-medium text-gray-900">{{ $user->name }}</div>
|
||||
<div class="text-sm text-gray-500">{{ $user->email }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<!-- Asignación de Roles -->
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<form class="update-roles" data-user-id="{{ $user->id }}">
|
||||
@csrf
|
||||
<select name="roles[]" multiple
|
||||
class="w-48 px-3 py-2 border rounded-lg shadow-sm focus:ring-blue-500 focus:border-blue-500"
|
||||
x-data="{}" x-init="tomSelect($el, {
|
||||
plugins: ['remove_button'],
|
||||
maxItems: 3
|
||||
})">
|
||||
@foreach($roles as $role)
|
||||
<option value="{{ $role->name }}" {{ $user->hasRole($role) ? 'selected' : '' }}>
|
||||
{{ $role->name }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</form>
|
||||
</td>
|
||||
|
||||
<!-- Asignación de Permisos -->
|
||||
<td class="px-6 py-4">
|
||||
<form class="update-permissions" data-user-id="{{ $user->id }}">
|
||||
@csrf
|
||||
<div class="grid grid-cols-2 gap-4 md:grid-cols-3">
|
||||
@foreach($permissions->groupBy('category') as $category => $perms)
|
||||
<div class="p-4 border rounded-lg">
|
||||
<h3 class="mb-2 text-sm font-semibold">{{ $category }}</h3>
|
||||
@foreach($perms as $permission)
|
||||
<label class="flex items-center space-x-2">
|
||||
<input type="checkbox"
|
||||
name="permissions[]"
|
||||
value="{{ $permission->name }}"
|
||||
{{ $user->hasDirectPermission($permission) ? 'checked' : '' }}
|
||||
class="rounded border-gray-300 text-blue-600 shadow-sm focus:ring-blue-500">
|
||||
<span class="text-sm">{{ $permission->name }}</span>
|
||||
</label>
|
||||
@endforeach
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@push('styles')
|
||||
<link href="https://cdn.jsdelivr.net/npm/tom-select@2.0.0/dist/css/tom-select.css" rel="stylesheet">
|
||||
@endpush
|
||||
|
||||
@push('scripts')
|
||||
<script src="https://cdn.jsdelivr.net/npm/tom-select@2.0.0/dist/js/tom-select.complete.min.js"></script>
|
||||
<script>
|
||||
// Búsqueda en tiempo real
|
||||
document.getElementById('search').addEventListener('input', function(e) {
|
||||
const searchTerm = e.target.value.toLowerCase();
|
||||
document.querySelectorAll('.user-row').forEach(row => {
|
||||
const text = row.textContent.toLowerCase();
|
||||
row.style.display = text.includes(searchTerm) ? '' : 'none';
|
||||
});
|
||||
});
|
||||
|
||||
// Actualización automática de roles
|
||||
document.querySelectorAll('select[name="roles[]"]').forEach(select => {
|
||||
new TomSelect(select, {
|
||||
plugins: ['remove_button'],
|
||||
onChange: function(value) {
|
||||
const form = select.closest('form');
|
||||
submitForm(form, 'roles');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Actualización automática de permisos
|
||||
document.querySelectorAll('input[name="permissions[]"]').forEach(checkbox => {
|
||||
checkbox.addEventListener('change', function() {
|
||||
const form = this.closest('form');
|
||||
submitForm(form, 'permissions');
|
||||
});
|
||||
});
|
||||
|
||||
// Función para enviar el formulario
|
||||
function submitForm(form, type) {
|
||||
const userId = form.dataset.userId;
|
||||
const formData = new FormData(form);
|
||||
|
||||
fetch(`/admin/users/${userId}/${type}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'X-CSRF-TOKEN': '{{ csrf_token() }}'
|
||||
},
|
||||
body: formData
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if(data.success) {
|
||||
showNotification('Cambios guardados exitosamente', 'success');
|
||||
} else {
|
||||
showNotification('Error al guardar los cambios', 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function showNotification(message, type) {
|
||||
// Implementar notificación (ej: Toast)
|
||||
console.log(`${type}: ${message}`);
|
||||
}
|
||||
</script>
|
||||
@endpush
|
||||
</x-layouts.app>
|
||||
89
resources/views/profile/edit.blade.php
Normal file
89
resources/views/profile/edit.blade.php
Normal file
@@ -0,0 +1,89 @@
|
||||
<x-app-layout>
|
||||
<div class="max-w-2xl mx-auto p-4 sm:p-6 lg:p-8">
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<h2 class="text-xl font-semibold mb-6">Editar Perfil</h2>
|
||||
|
||||
@if (session('status'))
|
||||
<div class="mb-4 text-green-600">
|
||||
{{ session('status') }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<form method="POST" action="{{ route('profile.update') }}">
|
||||
@csrf
|
||||
@method('patch')
|
||||
|
||||
<!-- Nombre -->
|
||||
<div class="mb-4">
|
||||
<x-input-label for="name" value="Nombre" />
|
||||
<x-text-input
|
||||
id="name"
|
||||
name="name"
|
||||
type="text"
|
||||
class="mt-1 block w-full"
|
||||
:value="old('name', $user->name)"
|
||||
required
|
||||
autofocus
|
||||
/>
|
||||
<x-input-error :messages="$errors->get('name')" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<!-- Email -->
|
||||
<div class="mb-4">
|
||||
<x-input-label for="email" value="Email" />
|
||||
<x-text-input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
class="mt-1 block w-full"
|
||||
:value="old('email', $user->email)"
|
||||
required
|
||||
/>
|
||||
<x-input-error :messages="$errors->get('email')" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<!-- Contraseña Actual -->
|
||||
<div class="mb-4">
|
||||
<x-input-label for="current_password" value="Contraseña Actual" />
|
||||
<x-text-input
|
||||
id="current_password"
|
||||
name="current_password"
|
||||
type="password"
|
||||
class="mt-1 block w-full"
|
||||
/>
|
||||
<x-input-error :messages="$errors->get('current_password')" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<!-- Nueva Contraseña -->
|
||||
<div class="mb-4">
|
||||
<x-input-label for="password" value="Nueva Contraseña" />
|
||||
<x-text-input
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
class="mt-1 block w-full"
|
||||
/>
|
||||
<x-input-error :messages="$errors->get('password')" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<!-- Confirmar Contraseña -->
|
||||
<div class="mb-6">
|
||||
<x-input-label for="password_confirmation" value="Confirmar Contraseña" />
|
||||
<x-text-input
|
||||
id="password_confirmation"
|
||||
name="password_confirmation"
|
||||
type="password"
|
||||
class="mt-1 block w-full"
|
||||
/>
|
||||
<x-input-error :messages="$errors->get('password_confirmation')" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end">
|
||||
<x-primary-button>
|
||||
Guardar Cambios
|
||||
</x-primary-button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</x-app-layout>
|
||||
386
resources/views/projects/create.blade.php
Normal file
386
resources/views/projects/create.blade.php
Normal file
@@ -0,0 +1,386 @@
|
||||
<!-- resources/views/projects/create.blade.php -->
|
||||
<x-layouts.app :title="__('Create Project')">
|
||||
<x-slot name="header">
|
||||
<h2 class="text-xl font-semibold leading-tight text-gray-800">
|
||||
Nuevo Proyecto
|
||||
</h2>
|
||||
</x-slot>
|
||||
|
||||
<div class="py-6">
|
||||
<div class="mx-auto max-w-7xl sm:px-6 lg:px-8">
|
||||
<div class="overflow-hidden bg-white shadow-sm sm:rounded-lg">
|
||||
<div class="p-6 bg-white border-b border-gray-200">
|
||||
<form method="POST" action="{{ route('projects.store') }}" enctype="multipart/form-data">
|
||||
@csrf
|
||||
<!-- Sección de Información Básica -->
|
||||
<div class="grid gap-6 mb-8 md:grid-cols-2">
|
||||
<!-- Columna Izquierda -->
|
||||
<div>
|
||||
<!-- Nombre -->
|
||||
<div class="mb-6">
|
||||
<x-label for="name" value="__('Nombre del Proyecto')" />
|
||||
<x-input id="name" class="block w-full mt-1"
|
||||
type="text" name="name" value="old('name')" required />
|
||||
@error('name')
|
||||
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- NombDescripción -->
|
||||
<div class="mb-6">
|
||||
<x-label for="description" value="__('Descripción')" />
|
||||
<x-textarea id="description" class="block w-full mt-1"
|
||||
name="description" rows="4" required>
|
||||
{{ old('description') }}
|
||||
</x-textarea>
|
||||
@error('description')
|
||||
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Editor Enriquecido -->
|
||||
<div class="mb-6">
|
||||
<x-label for="description" value="__('Descripción Detallada')" />
|
||||
<div id="editor-container">
|
||||
<!-- Barra de herramientas -->
|
||||
<div id="toolbar">
|
||||
<button class="ql-bold"></button>
|
||||
<button class="ql-italic"></button>
|
||||
<button class="ql-underline"></button>
|
||||
<button class="ql-strike"></button>
|
||||
<button class="ql-blockquote"></button>
|
||||
<button class="ql-code-block"></button>
|
||||
<button class="ql-link"></button>
|
||||
<button class="ql-image"></button>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Editor -->
|
||||
<div id="rich-editor">{!! old('description') !!}</div>
|
||||
</div>
|
||||
@error('description')
|
||||
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Estado -->
|
||||
<div class="mb-6">
|
||||
<x-label for="status" value="__('Estado del Proyecto')" />
|
||||
<x-select id="status" name="status" class="block w-full mt-1">
|
||||
<option value="active" {{ old('status') == 'active' ? 'selected' : '' }}>Activo</option>
|
||||
<option value="inactive" {{ old('status') == 'inactive' ? 'selected' : '' }}>Inactivo</option>
|
||||
</x-select>
|
||||
@error('status')
|
||||
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Imagen de Referencia -->
|
||||
<div class="mb-6">
|
||||
<x-label for="project_image" value="__('Imagen de Referencia')" />
|
||||
<div class="relative mt-1">
|
||||
<input type="file" id="project_image" name="project_image"
|
||||
class="block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4
|
||||
file:rounded-md file:border-0
|
||||
file:text-sm file:font-semibold
|
||||
file:bg-blue-50 file:text-blue-700
|
||||
hover:file:bg-blue-100"
|
||||
accept="image/*"
|
||||
onchange="previewImage(event)">
|
||||
<div class="mt-2" id="image-preview-container" style="display:none;">
|
||||
<img id="image-preview" class="object-cover h-48 w-full rounded-lg">
|
||||
</div>
|
||||
</div>
|
||||
@error('project_image')
|
||||
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Columna Derecha -->
|
||||
<div>
|
||||
<!-- Dirección -->
|
||||
<div class="mb-6">
|
||||
<x-label value="__('Ubicación')" />
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<x-input id="address" class="block w-full mt-1"
|
||||
type="text" name="address"
|
||||
value="old('address')"
|
||||
placeholder="Dirección" />
|
||||
@error('address')
|
||||
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<x-input id="postal_code" class="block w-full mt-1"
|
||||
type="text" name="postal_code"
|
||||
value="old('postal_code')"
|
||||
placeholder="Código Postal" />
|
||||
@error('postal_code')
|
||||
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<x-input id="province" class="block w-full mt-1"
|
||||
type="text" name="province"
|
||||
value="old('province')"
|
||||
placeholder="Provincia" />
|
||||
@error('province')
|
||||
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<x-select id="country" name="country" class="block w-full mt-1">
|
||||
<option value="">Seleccione País</option>
|
||||
@foreach(config('countries') as $code => $name)
|
||||
<option value="{{ $code }}" {{ old('country') == $code ? 'selected' : '' }}>
|
||||
{{ $name }}
|
||||
</option>
|
||||
@endforeach
|
||||
</x-select>
|
||||
@error('country')
|
||||
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mapa para Coordenadas -->
|
||||
<div class="mb-6">
|
||||
<x-label value="__('Seleccione Ubicación en el Mapa')" />
|
||||
<div id="map" class="h-64 rounded-lg border-2 border-gray-200"></div>
|
||||
<div class="grid grid-cols-2 gap-4 mt-2">
|
||||
<div>
|
||||
<x-label for="latitude" value="__('Latitud')" />
|
||||
<x-input id="latitude" name="latitude"
|
||||
type="number" step="any"
|
||||
value="old('latitude')" required />
|
||||
</div>
|
||||
<div>
|
||||
<x-label for="longitude" value="__('Longitud')" />
|
||||
<x-input id="longitude" name="longitude"
|
||||
type="number" step="any"
|
||||
value="old('longitude')" required />
|
||||
</div>
|
||||
</div>
|
||||
@error('latitude')
|
||||
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
@error('longitude')
|
||||
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sección Principal en 2 Columnas -->
|
||||
<div class="grid gap-6 mb-8 md:grid-cols-2">
|
||||
<!-- Columna Izquierda -->
|
||||
<div>
|
||||
<!-- Icono y Categorías -->
|
||||
<div class="mb-6">
|
||||
<x-label value="__('Identificación Visual')" />
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<x-label for="icon" value="__('Icono del Proyecto')" />
|
||||
<div class="relative mt-1">
|
||||
<select id="icon" name="icon"
|
||||
class="w-full rounded-md shadow-sm border-gray-300 focus:border-blue-300 focus:ring focus:ring-blue-200 focus:ring-opacity-50">
|
||||
<option value="">Seleccionar Icono</option>
|
||||
@foreach(config('project.icons') as $icon)
|
||||
<option value="{{ $icon }}"
|
||||
{{ old('icon') == $icon ? 'selected' : '' }}>
|
||||
<i class="fas fa-{{ $icon }} mr-2"></i>
|
||||
{{ Str::title($icon) }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<x-label for="categories" value="__('Categorías')" />
|
||||
<x-multiselect
|
||||
name="categories[]"
|
||||
:options="$categories"
|
||||
:selected="old('categories', [])"
|
||||
placeholder="Seleccione categorías"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Fechas Importantes -->
|
||||
<div class="grid grid-cols-2 gap-4 mb-6">
|
||||
<div>
|
||||
<x-label for="start_date" value="__('Fecha de Inicio')" />
|
||||
<x-input id="start_date" type="date"
|
||||
name="start_date" value="old('start_date')" />
|
||||
</div>
|
||||
<div>
|
||||
<x-label for="deadline" value="__('Fecha Límite')" />
|
||||
<x-input id="deadline" type="date"
|
||||
name="deadline" value="old('deadline')"
|
||||
min="{{ now()->format('Y-m-d') }}" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Columna Derecha -->
|
||||
<div>
|
||||
<!-- Archivos Adjuntos -->
|
||||
<div class="mb-6">
|
||||
<x-label value="__('Documentos Iniciales')" />
|
||||
<div x-data="{ files: [] }" class="mt-1">
|
||||
<div class="flex items-center justify-center px-6 pt-5 pb-6 border-2 border-gray-300 border-dashed rounded-md">
|
||||
<div class="text-center">
|
||||
<input type="file"
|
||||
name="documents[]"
|
||||
multiple
|
||||
class="absolute opacity-0"
|
||||
x-ref="fileInput"
|
||||
@change="files = Array.from($event.target.files)">
|
||||
|
||||
<template x-if="files.length === 0">
|
||||
<div>
|
||||
<x-icons.upload class="mx-auto h-12 w-12 text-gray-400" />
|
||||
<p class="mt-1 text-sm text-gray-600">
|
||||
Arrastra archivos o haz clic para subir
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template x-if="files.length > 0">
|
||||
<div class="space-y-2">
|
||||
<template x-for="(file, index) in files" :key="index">
|
||||
<div class="flex items-center text-sm text-gray-600">
|
||||
<x-icons icon="document" class="w-5 h-5 mr-2" />
|
||||
<span x-text="file.name"></span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mapa y Ubicación (Implementación previa) -->
|
||||
<!-- ... (Mantener sección de mapa y ubicación anterior) ... -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Resto del formulario -->
|
||||
<!-- ... (Mantener secciones anteriores de equipo, estado, etc) ... -->
|
||||
|
||||
<!-- Miembros del Equipo -->
|
||||
<div class="mb-6">
|
||||
Miembros del Equipo
|
||||
<x-label value="__('Miembros del Equipo')" />
|
||||
<div class="grid grid-cols-1 mt-2 gap-y-2 gap-x-4 sm:grid-cols-2">
|
||||
@foreach($users as $user)
|
||||
<label class="flex items-center space-x-2">
|
||||
<input type="checkbox"
|
||||
name="team[]"
|
||||
value="{{ $user->id }}"
|
||||
{{ in_array($user->id, old('team', [])) ? 'checked' : '' }}
|
||||
class="rounded border-gray-300 text-blue-600 shadow-sm focus:ring-blue-500">
|
||||
<span class="text-sm text-gray-700">{{ $user->name }}</span>
|
||||
</label>
|
||||
@endforeach
|
||||
</div>
|
||||
@error('team')
|
||||
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Botones de Acción -->
|
||||
<div class="flex justify-end mt-8 space-x-4">
|
||||
<a href="{{ route('projects.index') }}"
|
||||
class="px-4 py-2 text-gray-600 bg-white border border-gray-300 rounded-md shadow-sm hover:bg-gray-50">
|
||||
{{ __('Cancelar') }}
|
||||
</a>
|
||||
<x-button type="submit" class="bg-blue-600 hover:bg-blue-700">
|
||||
<x-icons icon="save" class="w-5 h-5 mr-2" />
|
||||
{{ __('Crear Proyecto') }}
|
||||
</x-button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@push('styles')
|
||||
<link href="https://cdn.jsdelivr.net/npm/@yaireo/tagify/dist/tagify.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/quill@2.0.3/dist/quill.snow.css" rel="stylesheet" />
|
||||
@endpush
|
||||
|
||||
@push('scripts')
|
||||
<script src="https://cdn.jsdelivr.net/npm/@yaireo/tagify"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/quill@2.0.3/dist/quill.js"></script>
|
||||
<script>
|
||||
// Editor Quill
|
||||
onst toolbarOptions = [
|
||||
['bold', 'italic', 'underline', 'strike'], // toggled buttons
|
||||
['blockquote', 'code-block'],
|
||||
['link', 'image', 'video', 'formula'],
|
||||
|
||||
[{ 'header': 1 }, { 'header': 2 }], // custom button values
|
||||
[{ 'list': 'ordered'}, { 'list': 'bullet' }, { 'list': 'check' }],
|
||||
[{ 'script': 'sub'}, { 'script': 'super' }], // superscript/subscript
|
||||
[{ 'indent': '-1'}, { 'indent': '+1' }], // outdent/indent
|
||||
[{ 'direction': 'rtl' }], // text direction
|
||||
|
||||
[{ 'size': ['small', false, 'large', 'huge'] }], // custom dropdown
|
||||
[{ 'header': [1, 2, 3, 4, 5, 6, false] }],
|
||||
|
||||
[{ 'color': [] }, { 'background': [] }], // dropdown with defaults from theme
|
||||
[{ 'font': [] }],
|
||||
[{ 'align': [] }],
|
||||
|
||||
['clean'] // remove formatting button
|
||||
];
|
||||
|
||||
const quill = new Quill('#rich-editor', {
|
||||
theme: 'snow',
|
||||
modules: {
|
||||
toolbar: toolbarOptions,
|
||||
placeholder: 'Description...',
|
||||
}
|
||||
});
|
||||
|
||||
// Tagify para categorías
|
||||
new Tagify(document.querySelector('[name="categories[]"]'), {
|
||||
enforceWhitelist: true,
|
||||
whitelist: @json($categories->pluck('name')),
|
||||
dropdown: {
|
||||
enabled: 1,
|
||||
maxItems: 5
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<script>
|
||||
function previewImage(event) {
|
||||
const reader = new FileReader();
|
||||
const preview = document.getElementById('image-preview');
|
||||
|
||||
reader.onload = function() {
|
||||
preview.innerHTML = `
|
||||
<img src="${reader.result}"
|
||||
class="object-cover w-full h-48 rounded-lg shadow-sm"
|
||||
alt="Vista previa de la imagen">
|
||||
`;
|
||||
}
|
||||
|
||||
if(event.target.files[0]) {
|
||||
reader.readAsDataURL(event.target.files[0]);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@endpush
|
||||
</x-layouts.app>
|
||||
119
resources/views/projects/index.blade.php
Normal file
119
resources/views/projects/index.blade.php
Normal file
@@ -0,0 +1,119 @@
|
||||
<!-- resources/views/projects/index.blade.php -->
|
||||
<x-layouts.app :title="__('Proyectos')">
|
||||
<x-slot name="header">
|
||||
<h2 class="text-xl font-semibold leading-tight text-gray-800">
|
||||
{{ __('Mis Proyectos') }}
|
||||
</h2>
|
||||
</x-slot>
|
||||
|
||||
<div class="py-6">
|
||||
<div class="mx-auto max-w-7xl sm:px-6 lg:px-8">
|
||||
<!-- Barra de búsqueda y creación -->
|
||||
<div class="flex flex-col justify-between mb-6 space-y-4 md:flex-row md:space-y-0">
|
||||
<div class="flex-1 max-w-md">
|
||||
<form action="{{ route('projects.index') }}" method="GET">
|
||||
<div class="relative">
|
||||
<input type="search" name="search" value="{{ request('search') }}"
|
||||
class="w-full pl-10 pr-4 py-2 rounded-lg border focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="Buscar proyectos...">
|
||||
<x-icons icon="search" class="absolute left-3 top-2.5 w-5 h-5 text-gray-400" />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@can('create', App\Models\Project::class)
|
||||
<a href="{{ route('projects.create') }}"
|
||||
class="flex items-center px-4 py-2 text-white bg-blue-600 rounded-lg hover:bg-blue-700">
|
||||
<x-icons icon="plus" class="w-5 h-5 mr-2" />
|
||||
{{ __('Nuevo Proyecto') }}
|
||||
</a>
|
||||
@endcan
|
||||
</div>
|
||||
|
||||
<!-- Listado de proyectos -->
|
||||
<div class="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
@forelse($projects as $project)
|
||||
<div class="overflow-hidden bg-white rounded-lg shadow">
|
||||
<div class="p-6">
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex-1">
|
||||
<h3 class="text-lg font-semibold text-gray-900">
|
||||
<a href="{{ route('projects.show', $project) }}"
|
||||
class="hover:text-blue-600 hover:underline">
|
||||
{{ $project->name }}
|
||||
</a>
|
||||
</h3>
|
||||
<p class="mt-2 text-sm text-gray-500">
|
||||
{{ Str::limit($project->description, 100) }}
|
||||
</p>
|
||||
</div>
|
||||
<x-dropdown align="right" width="48">
|
||||
<x-slot name="trigger">
|
||||
<button class="p-1 text-gray-400 hover:text-gray-600">
|
||||
<x-icons icon="dots-vertical class="w-5 h-5" />
|
||||
</button>
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="content">
|
||||
<x-dropdown-link href="{{ route('projects.show', $project) }}">
|
||||
<x-icons icon="eye" class="w-5 h-5 mr-2" /> Ver Detalles
|
||||
</x-dropdown-link>
|
||||
|
||||
@can('update', $project)
|
||||
<x-dropdown-link href="{{ route('projects.edit', $project) }}">
|
||||
<x-icons icon="pencil" class="w-5 h-5 mr-2" /> Editar
|
||||
</x-dropdown-link>
|
||||
@endcan
|
||||
|
||||
@can('delete', $project)
|
||||
<form method="POST" action="{{ route('projects.destroy', $project) }}">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<x-dropdown-link href="#"
|
||||
onclick="event.preventDefault(); this.closest('form').submit();"
|
||||
class="text-red-600 hover:bg-red-50">
|
||||
<x-icons icon="trash" class="w-5 h-5 mr-2" /> Eliminar
|
||||
</x-dropdown-link>
|
||||
</form>
|
||||
@endcan
|
||||
</x-slot>
|
||||
</x-dropdown>
|
||||
</div>
|
||||
|
||||
<!-- Metadatos del proyecto -->
|
||||
<div class="mt-4">
|
||||
<div class="flex items-center justify-between mt-3 text-sm">
|
||||
<span class="flex items-center text-gray-500">
|
||||
<x-icons icon="document" class="w-4 h-4 mr-1" />
|
||||
{{ $project->documents_count }} documentos
|
||||
</span>
|
||||
<span class="px-2 py-1 text-sm rounded-full
|
||||
{{ $project->status === 'active' ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800' }}">
|
||||
{{ __(Str::ucfirst($project->status)) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="mt-2 text-sm text-gray-500">
|
||||
<x-icons icon="calendar" class="w-4 h-4 mr-1 inline" />
|
||||
Creado {{ $project->created_at->diffForHumans() }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@empty
|
||||
<div class="p-6 text-center text-gray-500 col-span-full">
|
||||
<x-icons icon="folder-remove" class="w-12 h-12 mx-auto mb-4 text-gray-400" />
|
||||
{{ __('No tienes proyectos asignados.') }}
|
||||
</div>
|
||||
@endforelse
|
||||
</div>
|
||||
|
||||
<!-- Paginación -->
|
||||
@if($projects->hasPages())
|
||||
<div class="mt-6">
|
||||
{{ $projects->withQueryString()->links() }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</x-layouts.app>
|
||||
48
resources/views/projects/show.blade.php
Normal file
48
resources/views/projects/show.blade.php
Normal file
@@ -0,0 +1,48 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('content')
|
||||
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-6">
|
||||
<!-- Barra de herramientas con breadcrumbs -->
|
||||
<div class="mb-6">
|
||||
<nav class="flex" aria-label="Breadcrumb">
|
||||
<ol class="flex items-center space-x-2">
|
||||
<li>
|
||||
<div class="flex items-center">
|
||||
<a href="{{ route('projects.index') }}" class="text-gray-400 hover:text-gray-500">
|
||||
<x-icons.home class="h-5 w-5" />
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
@foreach($breadcrumbs as $crumb)
|
||||
<li>
|
||||
<div class="flex items-center">
|
||||
<x-icons.chevron-right class="h-5 w-5 text-gray-400" />
|
||||
@if(!$loop->last)
|
||||
<a href="{{ $crumb['url'] }}" class="ml-2 text-sm font-medium text-gray-500 hover:text-gray-700">
|
||||
{{ $crumb['name'] }}
|
||||
</a>
|
||||
@else
|
||||
<span class="ml-2 text-sm font-medium text-gray-700">{{ $crumb['name'] }}</span>
|
||||
@endif
|
||||
</div>
|
||||
</li>
|
||||
@endforeach
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<!-- Barra de herramientas principal -->
|
||||
<x-toolbar :project="$project" :current-folder="$currentFolder" />
|
||||
|
||||
<!-- Componente principal Livewire -->
|
||||
<livewire:project-show
|
||||
:project="$project"
|
||||
:current-folder="$currentFolder ?? null"
|
||||
wire:key="project-show-{{ $project->id }}-{{ $currentFolder->id ?? 'root' }}"
|
||||
/>
|
||||
|
||||
<!-- Modales -->
|
||||
@livewire('folder.create-modal', ['project' => $project, 'parentFolder' => $currentFolder ?? null])
|
||||
@livewire('document.upload-modal', ['project' => $project, 'currentFolder' => $currentFolder ?? null])
|
||||
</div>
|
||||
@endsection
|
||||
274
resources/views/welcome.blade.php
Normal file
274
resources/views/welcome.blade.php
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user