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);