diff --git a/app/Http/Controllers/V1/Admin/General/TimeFormatsController.php b/app/Http/Controllers/V1/Admin/General/TimeFormatsController.php new file mode 100644 index 00000000..5f1b101b --- /dev/null +++ b/app/Http/Controllers/V1/Admin/General/TimeFormatsController.php @@ -0,0 +1,22 @@ +json([ + 'time_formats' => TimeFormatter::get_list(), + ]); + } +} diff --git a/app/Http/Controllers/V1/Admin/Invoice/CloneInvoiceController.php b/app/Http/Controllers/V1/Admin/Invoice/CloneInvoiceController.php index 5e4321d2..e432f57d 100644 --- a/app/Http/Controllers/V1/Admin/Invoice/CloneInvoiceController.php +++ b/app/Http/Controllers/V1/Admin/Invoice/CloneInvoiceController.php @@ -46,6 +46,16 @@ class CloneInvoiceController extends Controller $exchange_rate = $invoice->exchange_rate; + $dateFormat = 'Y-m-d'; + $invoiceTimeEnabled = CompanySetting::getSetting( + 'invoice_use_time', + $request->header('company') + ); + + if ($invoiceTimeEnabled === 'YES') { + $dateFormat .= ' H:i'; + } + $newInvoice = Invoice::create([ 'invoice_date' => $date->format('Y-m-d'), 'due_date' => $due_date, diff --git a/app/Http/Requests/CompanySettingRequest.php b/app/Http/Requests/CompanySettingRequest.php index b6e267e2..2513cdc4 100644 --- a/app/Http/Requests/CompanySettingRequest.php +++ b/app/Http/Requests/CompanySettingRequest.php @@ -38,6 +38,15 @@ class CompanySettingRequest extends FormRequest 'carbon_date_format' => [ 'required', ], + 'moment_time_format' => [ + 'required', + ], + 'carbon_time_format' => [ + 'required', + ], + 'invoice_use_time' => [ + 'required', + ], ]; } } diff --git a/app/Models/Company.php b/app/Models/Company.php index 05a163ac..1b306f73 100644 --- a/app/Models/Company.php +++ b/app/Models/Company.php @@ -227,6 +227,9 @@ class Company extends Model implements HasMedia 'fiscal_year' => '1-12', 'carbon_date_format' => 'Y/m/d', 'moment_date_format' => 'YYYY/MM/DD', + 'carbon_time_format' => 'H:i', + 'moment_time_format' => 'HH:mm', + 'invoice_use_time' => 'NO', 'notification_email' => 'noreply@invoiceshelf.com', 'notify_invoice_viewed' => 'NO', 'notify_estimate_viewed' => 'NO', diff --git a/app/Models/Invoice.php b/app/Models/Invoice.php index f76dd5b5..ee5f193f 100644 --- a/app/Models/Invoice.php +++ b/app/Models/Invoice.php @@ -195,6 +195,12 @@ class Invoice extends Model implements HasMedia public function getFormattedInvoiceDateAttribute($value) { $dateFormat = CompanySetting::getSetting('carbon_date_format', $this->company_id); + $timeFormat = CompanySetting::getSetting('carbon_time_format', $this->company_id); + $invoiceTimeEnabled = CompanySetting::getSetting('invoice_use_time', $this->company_id); + + if ($invoiceTimeEnabled === 'YES') { + $dateFormat .= ' '.$timeFormat; + } return Carbon::parse($this->invoice_date)->translatedFormat($dateFormat); } diff --git a/app/Space/TimeFormatter.php b/app/Space/TimeFormatter.php new file mode 100644 index 00000000..5577f7fa --- /dev/null +++ b/app/Space/TimeFormatter.php @@ -0,0 +1,34 @@ + 'H:i', + 'moment_format' => 'HH:mm', + ], + [ + 'carbon_format' => 'g:i a', + 'moment_format' => 'h:mm a', + ], + ]; + + public static function get_list() + { + $new = []; + + foreach (static::$formats as $format) { + $new[] = [ + 'display_time' => Carbon::now()->format($format['carbon_format']), + 'carbon_format_value' => $format['carbon_format'], + 'moment_format_value' => $format['moment_format'], + ]; + } + + return $new; + } +} diff --git a/database/migrations/2024_08_08_173226_update_invoice_date_to_datetime_on_invoices_table.php b/database/migrations/2024_08_08_173226_update_invoice_date_to_datetime_on_invoices_table.php new file mode 100644 index 00000000..0dc697f0 --- /dev/null +++ b/database/migrations/2024_08_08_173226_update_invoice_date_to_datetime_on_invoices_table.php @@ -0,0 +1,28 @@ +datetime('invoice_date')->change(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('invoices', function (Blueprint $table) { + $table->date('invoice_date')->change(); + }); + } +}; diff --git a/lang/en.json b/lang/en.json index 0bde88cc..765d31af 100644 --- a/lang/en.json +++ b/lang/en.json @@ -864,6 +864,7 @@ "primary_currency": "Primary Currency", "timezone": "Time Zone", "date_format": "Date Format", + "time_format": "Time Format", "currencies": { "title": "Currencies", "currency": "Currency | Currencies", @@ -990,6 +991,7 @@ "delimiter_description": "Single character for specifying the boundary between 2 separate components. By default its set to -", "delimiter_param_label": "Delimiter Value", "date_format": "Date Format", + "time_format": "Time Format", "date_format_description": "A local date and time field which accepts a format parameter. The default format: 'Y' renders the current year.", "date_format_param_label": "Format", "sequence": "Sequence", @@ -1234,6 +1236,7 @@ "time_zone": "Time Zone", "fiscal_year": "Financial Year", "date_format": "Date Format", + "time_format": "Time Fromat", "discount_setting": "Discount Setting", "discount_per_item": "Discount Per Item ", "discount_setting_description": "Enable this if you want to add Discount to individual invoice items. By default, Discount is added directly to the invoice.", @@ -1246,6 +1249,7 @@ "select_language": "Select Language", "select_time_zone": "Select Time Zone", "select_date_format": "Select Date Format", + "select_time_format": "Select Time Format", "select_financial_year": "Select Financial Year", "recurring_invoice_status": "Recurring Invoice Status", "create_status": "Create Status", @@ -1254,6 +1258,8 @@ "update_status": "Update Status", "completed": "Completed", "company_currency_unchangeable": "Company currency cannot be changed", + "invoice_use_time": "Use time in invoices", + "invoice_use_time_description": "Enable this if you want to select exact invoice time.", "fiscal_years": { "january_december": "January - December", "february_january": "February - January", @@ -1422,6 +1428,7 @@ "time_zone": "Time Zone", "fiscal_year": "Financial Year", "date_format": "Date Format", + "time_format": "Time Format", "from_address": "From Address", "username": "Username", "next": "Next", diff --git a/resources/scripts/admin/stores/global.js b/resources/scripts/admin/stores/global.js index a32f0304..c88d7a19 100644 --- a/resources/scripts/admin/stores/global.js +++ b/resources/scripts/admin/stores/global.js @@ -21,6 +21,7 @@ export const useGlobalStore = (useWindow = false) => { // Global Lists timeZones: [], dateFormats: [], + timeFormats: [], currencies: [], countries: [], languages: [], @@ -156,6 +157,25 @@ export const useGlobalStore = (useWindow = false) => { }) }, + fetchTimeFormats() { + return new Promise((resolve, reject) => { + if (this.timeFormats.length) { + resolve(this.timeFormats) + } else { + axios + .get('/api/v1/time/formats') + .then((response) => { + this.timeFormats = response.data.time_formats + resolve(response) + }) + .catch((err) => { + handleError(err) + reject(err) + }) + } + }) + }, + fetchTimeZones() { return new Promise((resolve, reject) => { if (this.timeZones.length) { diff --git a/resources/scripts/admin/stores/invoice.js b/resources/scripts/admin/stores/invoice.js index 5b4d14f1..7f8a92ce 100644 --- a/resources/scripts/admin/stores/invoice.js +++ b/resources/scripts/admin/stores/invoice.js @@ -499,7 +499,13 @@ export const useInvoiceStore = (useWindow = false) => { this.newInvoice.sales_tax_address_type = companyStore.selectedCompanySettings.sales_tax_address_type this.newInvoice.discount_per_item = companyStore.selectedCompanySettings.discount_per_item - this.newInvoice.invoice_date = moment().format('YYYY-MM-DD') + + let dateFormat = 'YYYY-MM-DD'; + if (companyStore.selectedCompanySettings.invoice_use_time === 'YES') { + dateFormat += ' HH:mm' + } + + this.newInvoice.invoice_date = moment().format(dateFormat) if (companyStore.selectedCompanySettings.invoice_set_due_date_automatically === 'YES') { this.newInvoice.due_date = moment() .add(companyStore.selectedCompanySettings.invoice_due_date_days, 'days') diff --git a/resources/scripts/admin/views/invoices/create/InvoiceCreateBasicFields.vue b/resources/scripts/admin/views/invoices/create/InvoiceCreateBasicFields.vue index 9181b573..a67b09de 100644 --- a/resources/scripts/admin/views/invoices/create/InvoiceCreateBasicFields.vue +++ b/resources/scripts/admin/views/invoices/create/InvoiceCreateBasicFields.vue @@ -20,6 +20,8 @@ :content-loading="isLoading" :calendar-button="true" calendar-button-icon="calendar" + :enableTime="enableTime" + :time24hr="time24h" /> @@ -61,8 +63,10 @@ diff --git a/resources/scripts/admin/views/settings/PreferencesSetting.vue b/resources/scripts/admin/views/settings/PreferencesSetting.vue index 717d81cc..bbd7d6d3 100644 --- a/resources/scripts/admin/views/settings/PreferencesSetting.vue +++ b/resources/scripts/admin/views/settings/PreferencesSetting.vue @@ -80,10 +80,11 @@ label="display_date" value-prop="carbon_format_value" track-by="display_date" - searchable + :searchable="true" :invalid="v$.carbon_date_format.$error" class="w-full" /> + + + + + + + settingsForm.carbon_time_format, + (val) => { + if (val) { + const timeFormatObject = globalStore.timeFormats.find((d) => { + return d.carbon_format_value === val + }) + settingsForm.moment_time_format = timeFormatObject.moment_format_value + } + } +) + +const invoiceUseTimeField = computed({ + get: () => { + return settingsForm.invoice_use_time === 'YES' + }, + set: async (newValue) => { + const value = newValue ? 'YES' : 'NO' + let data = { + settings: { + invoice_use_time: value, + }, + } + settingsForm.invoice_use_time = value + } +}) + const discountPerItemField = computed({ get: () => { return settingsForm.discount_per_item === 'YES' @@ -271,12 +327,21 @@ const rules = computed(() => { moment_date_format: { required: helpers.withMessage(t('validation.required'), required), }, + carbon_time_format: { + required: helpers.withMessage(t('validation.required'), required), + }, + moment_time_format: { + required: helpers.withMessage(t('validation.required'), required), + }, time_zone: { required: helpers.withMessage(t('validation.required'), required), }, fiscal_year: { required: helpers.withMessage(t('validation.required'), required), }, + invoice_use_time: { + required: helpers.withMessage(t('validation.required'), required), + }, } }) @@ -292,6 +357,7 @@ async function setInitialData() { Promise.all([ globalStore.fetchCurrencies(), globalStore.fetchDateFormats(), + globalStore.fetchTimeFormats(), globalStore.fetchTimeZones(), ]).then(([res1]) => { isFetchingInitialData.value = false diff --git a/resources/scripts/components/base/BaseDatePicker.vue b/resources/scripts/components/base/BaseDatePicker.vue index e92745c7..d0797bdf 100644 --- a/resources/scripts/components/base/BaseDatePicker.vue +++ b/resources/scripts/components/base/BaseDatePicker.vue @@ -254,6 +254,14 @@ const carbonFormat = computed(() => { return companyStore.selectedCompanySettings?.carbon_date_format }) +const carbonFormatWithTime = computed(() => { + let format = companyStore.selectedCompanySettings?.carbon_date_format + if (companyStore.selectedCompanySettings?.invoice_use_time === 'YES') { + format += ' ' + companyStore.selectedCompanySettings?.carbon_time_format + } + return format.replace("g", "h").replace("a", "K"); +}) + const hasIconSlot = computed(() => { return !!slots.icon }) @@ -301,7 +309,7 @@ watch( config.altFormat = carbonFormat.value ? carbonFormat.value : 'd M Y' } else { config.altFormat = carbonFormat.value - ? `${carbonFormat.value} H:i ` + ? `${carbonFormatWithTime.value}` : 'd M Y H:i' } }, diff --git a/routes/api.php b/routes/api.php index 6146bda2..1146e25d 100644 --- a/routes/api.php +++ b/routes/api.php @@ -39,6 +39,7 @@ use App\Http\Controllers\V1\Admin\General\NotesController; use App\Http\Controllers\V1\Admin\General\NumberPlaceholdersController; use App\Http\Controllers\V1\Admin\General\SearchController; use App\Http\Controllers\V1\Admin\General\SearchUsersController; +use App\Http\Controllers\V1\Admin\General\TimeFormatsController; use App\Http\Controllers\V1\Admin\General\TimezonesController; use App\Http\Controllers\V1\Admin\Invoice\ChangeInvoiceStatusController; use App\Http\Controllers\V1\Admin\Invoice\CloneInvoiceController; @@ -230,6 +231,8 @@ Route::prefix('/v1')->group(function () { Route::get('/date/formats', DateFormatsController::class); + Route::get('/time/formats', TimeFormatsController::class); + Route::get('/next-number', NextNumberController::class); Route::get('/number-placeholders', NumberPlaceholdersController::class);