Updates for rejection

This commit is contained in:
David Bomba
2025-12-17 09:01:04 +11:00
parent 5f82a576b4
commit b689e8b6c2
18 changed files with 475 additions and 133 deletions

82
resources/js/clients/quotes/reject.js vendored Normal file
View File

@@ -0,0 +1,82 @@
/**
* Invoice Ninja (https://invoiceninja.com)
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
class Reject {
constructor() {
// Always show rejection modal for confirmation
// (Siempre mostrar modal de rechazo para confirmación)
}
submitForm() {
document.getElementById('reject-form').submit();
}
displayRejectModal() {
let displayRejectModal = document.getElementById("displayRejectModal");
if (displayRejectModal) {
displayRejectModal.removeAttribute("style");
}
}
hideRejectModal() {
let displayRejectModal = document.getElementById("displayRejectModal");
if (displayRejectModal) {
displayRejectModal.style.display = 'none';
}
}
handle() {
const rejectButton = document.getElementById('reject-button');
if (!rejectButton) return;
rejectButton.addEventListener('click', () => {
rejectButton.disabled = true;
// Re-enable the reject button after 2 seconds (Rehabilitar botón de rechazo después de 2 segundos)
setTimeout(() => {
rejectButton.disabled = false;
}, 2000);
// Always display the rejection modal for confirmation
// (Siempre mostrar el modal de rechazo para confirmación)
this.displayRejectModal();
});
// Handle confirm rejection button (Manejar botón de confirmar rechazo)
const rejectConfirmButton = document.getElementById('reject-confirm-button');
if (rejectConfirmButton) {
rejectConfirmButton.addEventListener('click', () => {
const rejectReason = document.getElementById('reject_reason');
// Set user input value if provided (optional)
// (Establecer valor de entrada del usuario si se proporciona - opcional)
if (rejectReason) {
const userInputField = document.querySelector('#reject-form input[name="user_input"]');
if (userInputField) {
userInputField.value = rejectReason.value;
}
}
this.hideRejectModal();
this.submitForm();
});
}
// Handle close/cancel button (Manejar botón de cerrar/cancelar)
const rejectCloseButton = document.getElementById('reject-close-button');
if (rejectCloseButton) {
rejectCloseButton.addEventListener('click', () => {
this.hideRejectModal();
});
}
}
}
new Reject().handle();

View File

@@ -1,33 +1,47 @@
<form action="{{ route('client.quotes.bulk') }}" method="post" id="approve-form">
@csrf
<input type="hidden" name="action" value="approve">
<input type="hidden" name="action" value="approve" id="quote-action">
<input type="hidden" name="process" value="true">
<input type="hidden" name="quotes[]" value="{{ $quote->hashed_id }}">
<input type="hidden" name="signature">
<input type="hidden" name="user_input" value="">
</form>
<form action="{{ route('client.quotes.bulk') }}" method="post" id="reject-form">
@csrf
<input type="hidden" name="action" value="reject">
<input type="hidden" name="process" value="true">
<input type="hidden" name="quotes[]" value="{{ $quote->hashed_id }}">
<input type="hidden" name="user_input" value="">
</form>
<div class="bg-white shadow sm:rounded-lg">
<div class="px-4 py-5 sm:p-6">
<div class="sm:flex sm:items-start sm:justify-between">
<div>
<h3 class="text-lg leading-6 font-medium text-gray-900">
{{ ctrans('texts.approve') }}
{{ ctrans('texts.approve') }} / {{ ctrans('texts.reject') }}
</h3>
</div>
<div class="mt-5 sm:mt-0 sm:ml-6 sm:flex-shrink-0 sm:flex sm:items-center">
@yield('quote-not-approved-right-side')
<div class="inline-flex rounded-md shadow-sm">
<input type="hidden" name="action" value="approve">
<button onclick="setTimeout(() => this.disabled = true, 0); return true;" type="button"
<div class="inline-flex rounded-md shadow-sm mr-2">
<button type="button"
onclick="document.querySelector('#approve-form [name=action]').value='approve'; setTimeout(() => this.disabled = true, 0); return true;"
class="button button-primary bg-primary"
id="approve-button">{{ ctrans('texts.approve') }}</button>
</div>
<div class="inline-flex rounded-md shadow-sm ">
<button type="button"
onclick="document.querySelector('#reject-form [name=action]').value='reject'; setTimeout(() => this.disabled = true, 0); return true;"
class="button button-secondary bg-red-500 text-white hover:bg-red-600"
id="reject-button">{{ ctrans('texts.reject') }}</button>
</div>
</div>
</div>
</div>
</div>
</form>

View File

@@ -0,0 +1,49 @@
{{-- Rejection Confirmation Modal (Modal de confirmación de rechazo) --}}
<div style="display: none;" id="displayRejectModal" class="fixed bottom-0 inset-x-0 px-4 pb-4 sm:inset-0 sm:flex sm:items-center sm:justify-center z-50">
<div x-show="open" x-transition:enter="ease-out duration-300" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100" x-transition:leave="ease-in duration-200" x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0" class="fixed inset-0 transition-opacity">
<div class="absolute inset-0 bg-gray-500 opacity-75"></div>
</div>
<div x-show="open" x-transition:enter="ease-out duration-300" x-transition:enter-start="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100" x-transition:leave="ease-in duration-200" x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100" x-transition:leave-end="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" class="bg-white rounded-lg px-4 pt-5 pb-4 overflow-hidden shadow-xl transform transition-all sm:max-w-lg sm:w-full sm:p-6">
<div class="sm:flex sm:items-start">
<div class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10">
<svg class="h-6 w-6 text-red-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z" />
</svg>
</div>
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left w-full">
<h3 class="text-xl leading-6 font-medium text-gray-900">
{{ ctrans('texts.reject_quote') }}
</h3>
<p class="mt-2 text-sm text-gray-500">
{{ ctrans('texts.reject_quote_confirmation') }}
</p>
<div class="mt-4">
<label for="reject_reason" class="block text-sm font-medium text-gray-700 mb-1">
{{ ctrans('texts.reason') }} ({{ ctrans('texts.optional') }})
</label>
<textarea
name="reject_reason"
id="reject_reason"
rows="3"
class="block w-full rounded-md border-gray-300 bg-gray-100 focus:border-primary-300 focus:bg-white focus:ring focus:ring-primary-200 focus:ring-opacity-50"
placeholder="{{ ctrans('texts.enter_reason') }}"></textarea>
</div>
</div>
</div>
<div class="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
<div class="flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto">
<button type="button" id="reject-confirm-button" class="button button-danger bg-red-500 hover:bg-red-600 text-white w-full sm:w-auto">
{{ ctrans('texts.reject') }}
</button>
</div>
<div class="mt-3 flex w-full rounded-md shadow-sm sm:mt-0 sm:w-auto">
<button type="button" class="button button-secondary w-full sm:w-auto" id="reject-close-button">
{{ ctrans('texts.cancel') }}
</button>
</div>
</div>
</div>
</div>

View File

@@ -22,6 +22,10 @@
<button type="submit" onclick="setTimeout(() => this.disabled = true, 0); return true;"
class="button button-primary bg-primary" name="action"
value="approve">{{ ctrans('texts.approve') }}</button>
<button type="submit" onclick="setTimeout(() => this.disabled = true, 0); return true;"
class="button button-primary bg-primary" name="action"
value="reject">{{ ctrans('texts.reject') }}</button>
</form>
</div>

View File

@@ -0,0 +1,90 @@
@extends('portal.ninja2020.layout.app')
@section('meta_title', ctrans('texts.reject'))
@push('head')
<meta name="accept-user-input" content="true">
@endpush
@section('body')
<form action="{{ route('client.quotes.bulk') }}" method="post" id="reject-form">
@csrf
<input type="hidden" name="action" value="reject">
<input type="hidden" name="process" value="true">
<input type="hidden" name="user_input" value="">
@foreach($quotes as $quote)
<input type="hidden" name="quotes[]" value="{{ $quote->hashed_id }}">
@endforeach
</form>
<div class="container mx-auto">
<div class="grid grid-cols-6 gap-4">
<div class="col-span-6 md:col-start-2 md:col-span-4">
<div class="flex justify-end">
<div class="flex justify-end mb-2">
<div class="relative inline-block text-left">
<div>
<div class="rounded-md shadow-sm">
<button type="button" id="reject-button" onclick="setTimeout(() => this.disabled = true, 0); return true;"
class="inline-flex justify-center w-full rounded-md border border-gray-300 px-4 py-2 bg-white text-sm leading-5 font-medium text-gray-700 hover:text-gray-500 focus:outline-none focus:border-blue-300 focus:ring-blue active:bg-gray-50 active:text-gray-800 transition ease-in-out duration-150">
{{ ctrans('texts.reject') }}
</button>
</div>
</div>
</div>
</div>
</div>
@foreach($quotes as $quote)
<div class="bg-white shadow overflow-hidden sm:rounded-lg mb-4">
<div class="px-4 py-5 border-b border-gray-200 sm:px-6">
<h3 class="text-lg leading-6 font-medium text-gray-900">
{{ ctrans('texts.quote') }}
<a class="button-link text-primary" href="{{ route('client.quote.show', $quote->hashed_id) }}">
({{ $quote->number }})
</a>
</h3>
<p class="mt-1 max-w-2xl text-sm leading-5 text-gray-500" translate>
</p>
</div>
<div>
<dl>
<div class="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm leading-5 font-medium text-gray-500">
{{ ctrans('texts.quote_number') }}
</dt>
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
{{ $quote->number }}
</dd>
</div>
<div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm leading-5 font-medium text-gray-500">
{{ ctrans('texts.quote_date') }}
</dt>
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
{{ $quote->translateDate($quote->date, $quote->client->date_format(), $quote->client->locale()) }}
</dd>
</div>
<div class="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm leading-5 font-medium text-gray-500">
{{ ctrans('texts.amount') }}
</dt>
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
{{ App\Utils\Number::formatMoney($quote->amount, $quote->client) }}
</dd>
</div>
</dl>
</div>
</div>
@endforeach
</div>
</div>
</div>
@endsection
@section('footer')
@include('portal.ninja2020.quotes.includes.user-input')
@endsection
@push('footer')
@vite('resources/js/clients/quotes/reject.js')
@endpush

View File

@@ -10,7 +10,7 @@
@section('body')
@if(!$quote->isApproved() && $client->getSetting('custom_message_unapproved_quote'))
@if(!$quote->isApproved() && !$quote->isRejected() && $client->getSetting('custom_message_unapproved_quote'))
@component('portal.ninja2020.components.message')
<pre>{{ $client->getSetting('custom_message_unapproved_quote') }}</pre>
@endcomponent
@@ -31,13 +31,13 @@
</h3>
</div>
@if($quote->invoice()->exists())
<div class="mt-5 sm:mt-0 sm:ml-6 flex justify-end">
<div class="inline-flex rounded-md shadow-sm">
<a class="button button-primary bg-primary" href="/client/invoices/{{ $quote->invoice->hashed_id }}">{{ ctrans('texts.view_invoice') }}</a>
</div>
</div>
@endif
@if($quote->invoice()->exists())
<div class="mt-5 sm:mt-0 sm:ml-6 flex justify-end">
<div class="inline-flex rounded-md shadow-sm">
<a class="button button-primary bg-primary" href="/client/invoices/{{ $quote->invoice->hashed_id }}">{{ ctrans('texts.view_invoice') }}</a>
</div>
</div>
@endif
</div>
</div>
</div>
@@ -55,7 +55,19 @@
</div>
</div>
</div>
@elseif($quote->status_id == \App\Models\Quote::STATUS_REJECTED)
<div class="bg-white shadow sm:rounded-lg mb-4">
<div class="px-4 py-5 sm:p-6">
<div class="sm:flex sm:items-start sm:justify-between">
<div>
<h3 class="text-lg leading-6 font-medium text-gray-900">
{{ ctrans('texts.rejected') }}
</h3>
</div>
</div>
</div>
</div>
@else
<div class="bg-white shadow sm:rounded-lg mb-4">
@@ -78,13 +90,14 @@
@section('footer')
@include('portal.ninja2020.quotes.includes.user-input')
@include('portal.ninja2020.quotes.includes.reject-input')
@include('portal.ninja2020.invoices.includes.terms', ['entities' => [$quote], 'variables' => $variables, 'entity_type' => ctrans('texts.quote')])
@include('portal.ninja2020.invoices.includes.signature')
@endsection
@push('head')
@vite('resources/js/clients/quotes/approve.js')
@vite('resources/js/clients/quotes/reject.js')
<script type="text/javascript" defer>
document.addEventListener('DOMContentLoaded', () => {