From b32c334a71a2e8601a3ea5fe29b2a26e0d93fced Mon Sep 17 00:00:00 2001 From: Yannic Inselmann Date: Sat, 5 Apr 2025 12:01:06 +0200 Subject: [PATCH] feat: default notes (#263) * feat: default notes * feat: include default invoice note in recurring invoice * feat: use default export in tw config * fix: test and naming * fix: consistent ui for switch in note modal * feat: little text improvements --- .../V1/Admin/General/NotesController.php | 18 +++++++++ app/Http/Requests/NotesRequest.php | 3 ++ app/Http/Resources/NoteResource.php | 1 + database/factories/NoteFactory.php | 1 + ...5_211404_add_is_default_to_notes_table.php | 28 +++++++++++++ lang/de.json | 2 + lang/en.json | 2 + .../components/modal-components/NoteModal.vue | 5 +++ resources/scripts/admin/stores/estimate.js | 4 ++ resources/scripts/admin/stores/invoice.js | 6 ++- resources/scripts/admin/stores/note.js | 6 +++ resources/scripts/admin/stores/payment.js | 4 ++ .../scripts/admin/stores/recurring-invoice.js | 4 ++ .../views/invoices/create/InvoiceCreate.vue | 3 ++ .../admin/views/settings/NotesSetting.vue | 10 ++--- tailwind.config.js | 39 +++++++++---------- 16 files changed, 110 insertions(+), 26 deletions(-) create mode 100644 database/migrations/2025_01_05_211404_add_is_default_to_notes_table.php diff --git a/app/Http/Controllers/V1/Admin/General/NotesController.php b/app/Http/Controllers/V1/Admin/General/NotesController.php index cef1d905..88c1110e 100644 --- a/app/Http/Controllers/V1/Admin/General/NotesController.php +++ b/app/Http/Controllers/V1/Admin/General/NotesController.php @@ -41,6 +41,15 @@ class NotesController extends Controller $note = Note::create($request->getNotesPayload()); + if ($note->is_default) { + Note::where('id', '!=', $note->id) + ->where('type', $note->type) + ->where('is_default', true) + ->update([ + 'is_default' => false, + ]); + } + return new NoteResource($note); } @@ -68,6 +77,15 @@ class NotesController extends Controller $note->update($request->getNotesPayload()); + if ($note->is_default) { + Note::where('id', '!=', $note->id) + ->where('type', $note->type) + ->where('is_default', true) + ->update([ + 'is_default' => false, + ]); + } + return new NoteResource($note); } diff --git a/app/Http/Requests/NotesRequest.php b/app/Http/Requests/NotesRequest.php index a1c16be9..9dc54ffe 100644 --- a/app/Http/Requests/NotesRequest.php +++ b/app/Http/Requests/NotesRequest.php @@ -33,6 +33,9 @@ class NotesRequest extends FormRequest 'notes' => [ 'required', ], + 'is_default' => [ + 'required', + ], ]; if ($this->isMethod('PUT')) { diff --git a/app/Http/Resources/NoteResource.php b/app/Http/Resources/NoteResource.php index 337863c3..1619d333 100644 --- a/app/Http/Resources/NoteResource.php +++ b/app/Http/Resources/NoteResource.php @@ -18,6 +18,7 @@ class NoteResource extends JsonResource 'type' => $this->type, 'name' => $this->name, 'notes' => $this->notes, + 'is_default' => $this->is_default, 'company' => $this->when($this->company()->exists(), function () { return new CompanyResource($this->company); }), diff --git a/database/factories/NoteFactory.php b/database/factories/NoteFactory.php index 8a557781..3637faae 100644 --- a/database/factories/NoteFactory.php +++ b/database/factories/NoteFactory.php @@ -25,6 +25,7 @@ class NoteFactory extends Factory 'name' => $this->faker->word(), 'notes' => $this->faker->text(), 'company_id' => User::find(1)->companies()->first()->id, + 'is_default' => $this->faker->boolean(), ]; } } diff --git a/database/migrations/2025_01_05_211404_add_is_default_to_notes_table.php b/database/migrations/2025_01_05_211404_add_is_default_to_notes_table.php new file mode 100644 index 00000000..5c0201f8 --- /dev/null +++ b/database/migrations/2025_01_05_211404_add_is_default_to_notes_table.php @@ -0,0 +1,28 @@ +boolean('is_default')->default(false); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('notes', function (Blueprint $table) { + $table->dropColumn('is_default'); + }); + } +}; diff --git a/lang/de.json b/lang/de.json index e3abf3dc..bb25d8de 100644 --- a/lang/de.json +++ b/lang/de.json @@ -1087,6 +1087,8 @@ "description": "Sparen Sie Zeit, indem Sie Notizen erstellen und diese auf Ihren Rechnungen, Angeboten und Zahlungen wiederverwenden.", "notes": "Hinweise", "type": "Art", + "is_default": "Standardmäßig auswählen", + "is_default_description": "Diese Notiz wird standardmäßig in neuen Rechnungen ausgewählt.", "add_note": "Notiz hinzufügen", "add_new_note": "Neue Notiz hinzufügen", "name": "Name", diff --git a/lang/en.json b/lang/en.json index 3a0510e7..0d4caaa0 100644 --- a/lang/en.json +++ b/lang/en.json @@ -1087,6 +1087,8 @@ "description": "Save time by creating notes and reusing them on your invoices, estimates & payments.", "notes": "Notes", "type": "Type", + "is_default": "Select by default", + "is_default_description": "This note will be selected by default in new invoices.", "add_note": "Add Note", "add_new_note": "Add New Note", "name": "Name", diff --git a/resources/scripts/admin/components/modal-components/NoteModal.vue b/resources/scripts/admin/components/modal-components/NoteModal.vue index 36881d45..4e5d81bf 100644 --- a/resources/scripts/admin/components/modal-components/NoteModal.vue +++ b/resources/scripts/admin/components/modal-components/NoteModal.vue @@ -45,6 +45,11 @@ class="mt-2" /> + { const defineStoreFunc = useWindow ? window.pinia.defineStore : defineStore @@ -554,6 +555,7 @@ export const useEstimateStore = (useWindow = false) => { const taxTypeStore = useTaxTypeStore() const route = useRoute() const userStore = useUserStore() + const notesStore = useNotesStore() this.isFetchingInitialSettings = true this.newEstimate.selectedCurrency = companyStore.selectedCompanyCurrency @@ -567,6 +569,8 @@ export const useEstimateStore = (useWindow = false) => { let editActions = [] if (!isEdit) { + await notesStore.fetchNotes() + this.newEstimate.notes = notesStore.getDefaultNoteForType('Estimate')?.notes this.newEstimate.tax_per_item = companyStore.selectedCompanySettings.tax_per_item this.newEstimate.sales_tax_type = companyStore.selectedCompanySettings.sales_tax_type diff --git a/resources/scripts/admin/stores/invoice.js b/resources/scripts/admin/stores/invoice.js index 7f8a92ce..ba8f8b7d 100644 --- a/resources/scripts/admin/stores/invoice.js +++ b/resources/scripts/admin/stores/invoice.js @@ -15,6 +15,7 @@ import { useTaxTypeStore } from './tax-type' import { useCompanyStore } from './company' import { useItemStore } from './item' import { useUserStore } from './user' +import { useNotesStore } from './note' export const useInvoiceStore = (useWindow = false) => { const defineStoreFunc = useWindow ? window.pinia.defineStore : defineStore @@ -479,6 +480,7 @@ export const useInvoiceStore = (useWindow = false) => { const taxTypeStore = useTaxTypeStore() const route = useRoute() const userStore = useUserStore() + const notesStore = useNotesStore() this.isFetchingInitialSettings = true @@ -491,8 +493,10 @@ export const useInvoiceStore = (useWindow = false) => { } let editActions = [] - + if (!isEdit) { + await notesStore.fetchNotes() + this.newInvoice.notes = notesStore.getDefaultNoteForType('Invoice')?.notes this.newInvoice.tax_per_item = companyStore.selectedCompanySettings.tax_per_item this.newInvoice.sales_tax_type = companyStore.selectedCompanySettings.sales_tax_type diff --git a/resources/scripts/admin/stores/note.js b/resources/scripts/admin/stores/note.js index f05f2570..724617a9 100644 --- a/resources/scripts/admin/stores/note.js +++ b/resources/scripts/admin/stores/note.js @@ -14,6 +14,7 @@ export const useNotesStore = (useWindow = false) => { currentNote: { id: null, type: '', + is_default: false, name: '', notes: '', }, @@ -24,9 +25,14 @@ export const useNotesStore = (useWindow = false) => { }, actions: { + getDefaultNoteForType(type) { + return this.notes.find((note) => note.type === type && note.is_default) + }, + resetCurrentNote() { this.currentNote = { type: '', + is_default: false, name: '', notes: '', } diff --git a/resources/scripts/admin/stores/payment.js b/resources/scripts/admin/stores/payment.js index b6fb0fb2..2cf1284f 100644 --- a/resources/scripts/admin/stores/payment.js +++ b/resources/scripts/admin/stores/payment.js @@ -6,6 +6,7 @@ import { useCompanyStore } from './company' import { useNotificationStore } from '@/scripts/stores/notification' import paymentStub from '../stub/payment' import { handleError } from '@/scripts/helpers/error-handling' +import { useNotesStore } from './note' export const usePaymentStore = (useWindow = false) => { const defineStoreFunc = useWindow ? window.pinia.defineStore : defineStore @@ -56,6 +57,7 @@ export const usePaymentStore = (useWindow = false) => { actions: { fetchPaymentInitialData(isEdit) { const companyStore = useCompanyStore() + const notesStore = useNotesStore() const route = useRoute() this.isFetchingInitialData = true @@ -80,6 +82,8 @@ export const usePaymentStore = (useWindow = false) => { // On Create else if (!isEdit && res2.data) { + await notesStore.fetchNotes() + this.currentPayment.notes = notesStore.getDefaultNoteForType('Payment')?.notes this.currentPayment.payment_date = moment().format('YYYY-MM-DD') this.currentPayment.payment_number = res2.data.nextNumber this.currentPayment.currency = diff --git a/resources/scripts/admin/stores/recurring-invoice.js b/resources/scripts/admin/stores/recurring-invoice.js index 8db9cea1..0aa22cce 100644 --- a/resources/scripts/admin/stores/recurring-invoice.js +++ b/resources/scripts/admin/stores/recurring-invoice.js @@ -14,6 +14,7 @@ import moment from 'moment' import _ from 'lodash' import { useInvoiceStore } from './invoice' import { useNotificationStore } from '@/scripts/stores/notification' +import { useNotesStore } from './note' export const useRecurringInvoiceStore = (useWindow = false) => { const defineStoreFunc = useWindow ? window.pinia.defineStore : defineStore @@ -334,6 +335,7 @@ export const useRecurringInvoiceStore = (useWindow = false) => { const invoiceStore = useInvoiceStore() const taxTypeStore = useTaxTypeStore() const route = useRoute() + const notesStore = useNotesStore() this.isFetchingInitialSettings = true this.newRecurringInvoice.currency = companyStore.selectedCompanyCurrency @@ -348,6 +350,8 @@ export const useRecurringInvoiceStore = (useWindow = false) => { // on create if (!isEdit) { + await notesStore.fetchNotes() + this.newRecurringInvoice.notes = notesStore.getDefaultNoteForType('Invoice')?.notes this.newRecurringInvoice.tax_per_item = companyStore.selectedCompanySettings.tax_per_item this.newRecurringInvoice.discount_per_item = diff --git a/resources/scripts/admin/views/invoices/create/InvoiceCreate.vue b/resources/scripts/admin/views/invoices/create/InvoiceCreate.vue index cc2b0bc4..3a257737 100644 --- a/resources/scripts/admin/views/invoices/create/InvoiceCreate.vue +++ b/resources/scripts/admin/views/invoices/create/InvoiceCreate.vue @@ -151,6 +151,7 @@ import { cloneDeep } from 'lodash' import { useInvoiceStore } from '@/scripts/admin/stores/invoice' import { useModuleStore } from '@/scripts/admin/stores/module' +import { useNotesStore } from '@/scripts/admin/stores/note' import { useCompanyStore } from '@/scripts/admin/stores/company' import { useCustomFieldStore } from '@/scripts/admin/stores/custom-field' @@ -169,6 +170,8 @@ const invoiceStore = useInvoiceStore() const companyStore = useCompanyStore() const customFieldStore = useCustomFieldStore() const moduleStore = useModuleStore() +const notesStore = useNotesStore() + const { t } = useI18n() let route = useRoute() let router = useRouter() diff --git a/resources/scripts/admin/views/settings/NotesSetting.vue b/resources/scripts/admin/views/settings/NotesSetting.vue index 320e3d5b..4b4c934d 100644 --- a/resources/scripts/admin/views/settings/NotesSetting.vue +++ b/resources/scripts/admin/views/settings/NotesSetting.vue @@ -31,6 +31,10 @@ :load-data="refreshTable" /> + @@ -42,9 +46,7 @@ import { computed, reactive, ref } from 'vue' import { useI18n } from 'vue-i18n' import { useModalStore } from '@/scripts/stores/modal' -import { useDialogStore } from '@/scripts/stores/dialog' import { useNotesStore } from '@/scripts/admin/stores/note' -import { useNotificationStore } from '@/scripts/stores/notification' import NoteDropdown from '@/scripts/admin/components/dropdowns/NoteIndexDropdown.vue' import NoteModal from '@/scripts/admin/components/modal-components/NoteModal.vue' import { useUserStore } from '@/scripts/admin/stores/user' @@ -53,9 +55,7 @@ import abilities from '@/scripts/admin/stub/abilities' const { t } = useI18n() const modalStore = useModalStore() -const dialogStore = useDialogStore() const noteStore = useNotesStore() -const notificationStore = useNotificationStore() const userStore = useUserStore() const table = ref('') @@ -66,7 +66,7 @@ const notesColumns = computed(() => { key: 'name', label: t('settings.customization.notes.name'), thClass: 'extra', - tdClass: 'font-medium text-gray-900', + tdClass: 'font-medium text-gray-900 flex gap-1 items-center', }, { key: 'type', diff --git a/tailwind.config.js b/tailwind.config.js index dfe331a5..49e0ae57 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -1,5 +1,5 @@ -const colors = require('tailwindcss/colors') -const svgToDataUri = require('mini-svg-data-uri') +import { red, teal, slate } from 'tailwindcss/colors'; +import svgToDataUri from 'mini-svg-data-uri'; function withOpacityValue(cssVariable) { return ({ opacityVariable, opacityValue }) => { @@ -13,11 +13,13 @@ function withOpacityValue(cssVariable) { }; } -module.exports = { - content: [ - './resources/views/**/*.php', - './resources/scripts/**/*.js', - './resources/scripts/**/*.vue', +export default { + plugins: [ + require('@tailwindcss/forms'), + require('@tailwindcss/typography'), + require('@tailwindcss/aspect-ratio'), + require('tailwind-scrollbar'), + require('@rvxlab/tailwind-plugin-ios-full-height') ], theme: { extend: { @@ -35,9 +37,9 @@ module.exports = { 900: withOpacityValue('--color-primary-900'), }, black: '#040405', - red: colors.red, - teal: colors.teal, - gray: colors.slate, + red: red, + teal: teal, + gray: slate, }, spacing: { 88: '22rem', @@ -45,8 +47,8 @@ module.exports = { backgroundImage: (theme) => ({ 'multiselect-caret': `url("${svgToDataUri( ` - -` + + ` )}")`, 'multiselect-spinner': `url("${svgToDataUri( `