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
This commit is contained in:
Yannic Inselmann
2025-04-05 12:01:06 +02:00
committed by GitHub
parent 2aa17513e1
commit b32c334a71
16 changed files with 110 additions and 26 deletions

View File

@@ -41,6 +41,15 @@ class NotesController extends Controller
$note = Note::create($request->getNotesPayload()); $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); return new NoteResource($note);
} }
@@ -68,6 +77,15 @@ class NotesController extends Controller
$note->update($request->getNotesPayload()); $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); return new NoteResource($note);
} }

View File

@@ -33,6 +33,9 @@ class NotesRequest extends FormRequest
'notes' => [ 'notes' => [
'required', 'required',
], ],
'is_default' => [
'required',
],
]; ];
if ($this->isMethod('PUT')) { if ($this->isMethod('PUT')) {

View File

@@ -18,6 +18,7 @@ class NoteResource extends JsonResource
'type' => $this->type, 'type' => $this->type,
'name' => $this->name, 'name' => $this->name,
'notes' => $this->notes, 'notes' => $this->notes,
'is_default' => $this->is_default,
'company' => $this->when($this->company()->exists(), function () { 'company' => $this->when($this->company()->exists(), function () {
return new CompanyResource($this->company); return new CompanyResource($this->company);
}), }),

View File

@@ -25,6 +25,7 @@ class NoteFactory extends Factory
'name' => $this->faker->word(), 'name' => $this->faker->word(),
'notes' => $this->faker->text(), 'notes' => $this->faker->text(),
'company_id' => User::find(1)->companies()->first()->id, 'company_id' => User::find(1)->companies()->first()->id,
'is_default' => $this->faker->boolean(),
]; ];
} }
} }

View File

@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('notes', function (Blueprint $table) {
$table->boolean('is_default')->default(false);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('notes', function (Blueprint $table) {
$table->dropColumn('is_default');
});
}
};

View File

@@ -1087,6 +1087,8 @@
"description": "Sparen Sie Zeit, indem Sie Notizen erstellen und diese auf Ihren Rechnungen, Angeboten und Zahlungen wiederverwenden.", "description": "Sparen Sie Zeit, indem Sie Notizen erstellen und diese auf Ihren Rechnungen, Angeboten und Zahlungen wiederverwenden.",
"notes": "Hinweise", "notes": "Hinweise",
"type": "Art", "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_note": "Notiz hinzufügen",
"add_new_note": "Neue Notiz hinzufügen", "add_new_note": "Neue Notiz hinzufügen",
"name": "Name", "name": "Name",

View File

@@ -1087,6 +1087,8 @@
"description": "Save time by creating notes and reusing them on your invoices, estimates & payments.", "description": "Save time by creating notes and reusing them on your invoices, estimates & payments.",
"notes": "Notes", "notes": "Notes",
"type": "Type", "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_note": "Add Note",
"add_new_note": "Add New Note", "add_new_note": "Add New Note",
"name": "Name", "name": "Name",

View File

@@ -45,6 +45,11 @@
class="mt-2" class="mt-2"
/> />
</BaseInputGroup> </BaseInputGroup>
<BaseSwitchSection
v-model="noteStore.currentNote.is_default"
:title="$t('settings.customization.notes.is_default')"
:description="$t('settings.customization.notes.is_default_description')"
_ />
<BaseInputGroup <BaseInputGroup
:label="$t('settings.customization.notes.notes')" :label="$t('settings.customization.notes.notes')"

View File

@@ -14,6 +14,7 @@ import estimateStub from '../stub/estimate'
import estimateItemStub from '../stub/estimate-item' import estimateItemStub from '../stub/estimate-item'
import taxStub from '../stub/tax' import taxStub from '../stub/tax'
import { useUserStore } from './user' import { useUserStore } from './user'
import { useNotesStore } from './note'
export const useEstimateStore = (useWindow = false) => { export const useEstimateStore = (useWindow = false) => {
const defineStoreFunc = useWindow ? window.pinia.defineStore : defineStore const defineStoreFunc = useWindow ? window.pinia.defineStore : defineStore
@@ -554,6 +555,7 @@ export const useEstimateStore = (useWindow = false) => {
const taxTypeStore = useTaxTypeStore() const taxTypeStore = useTaxTypeStore()
const route = useRoute() const route = useRoute()
const userStore = useUserStore() const userStore = useUserStore()
const notesStore = useNotesStore()
this.isFetchingInitialSettings = true this.isFetchingInitialSettings = true
this.newEstimate.selectedCurrency = companyStore.selectedCompanyCurrency this.newEstimate.selectedCurrency = companyStore.selectedCompanyCurrency
@@ -567,6 +569,8 @@ export const useEstimateStore = (useWindow = false) => {
let editActions = [] let editActions = []
if (!isEdit) { if (!isEdit) {
await notesStore.fetchNotes()
this.newEstimate.notes = notesStore.getDefaultNoteForType('Estimate')?.notes
this.newEstimate.tax_per_item = this.newEstimate.tax_per_item =
companyStore.selectedCompanySettings.tax_per_item companyStore.selectedCompanySettings.tax_per_item
this.newEstimate.sales_tax_type = companyStore.selectedCompanySettings.sales_tax_type this.newEstimate.sales_tax_type = companyStore.selectedCompanySettings.sales_tax_type

View File

@@ -15,6 +15,7 @@ import { useTaxTypeStore } from './tax-type'
import { useCompanyStore } from './company' import { useCompanyStore } from './company'
import { useItemStore } from './item' import { useItemStore } from './item'
import { useUserStore } from './user' import { useUserStore } from './user'
import { useNotesStore } from './note'
export const useInvoiceStore = (useWindow = false) => { export const useInvoiceStore = (useWindow = false) => {
const defineStoreFunc = useWindow ? window.pinia.defineStore : defineStore const defineStoreFunc = useWindow ? window.pinia.defineStore : defineStore
@@ -479,6 +480,7 @@ export const useInvoiceStore = (useWindow = false) => {
const taxTypeStore = useTaxTypeStore() const taxTypeStore = useTaxTypeStore()
const route = useRoute() const route = useRoute()
const userStore = useUserStore() const userStore = useUserStore()
const notesStore = useNotesStore()
this.isFetchingInitialSettings = true this.isFetchingInitialSettings = true
@@ -493,6 +495,8 @@ export const useInvoiceStore = (useWindow = false) => {
let editActions = [] let editActions = []
if (!isEdit) { if (!isEdit) {
await notesStore.fetchNotes()
this.newInvoice.notes = notesStore.getDefaultNoteForType('Invoice')?.notes
this.newInvoice.tax_per_item = this.newInvoice.tax_per_item =
companyStore.selectedCompanySettings.tax_per_item companyStore.selectedCompanySettings.tax_per_item
this.newInvoice.sales_tax_type = companyStore.selectedCompanySettings.sales_tax_type this.newInvoice.sales_tax_type = companyStore.selectedCompanySettings.sales_tax_type

View File

@@ -14,6 +14,7 @@ export const useNotesStore = (useWindow = false) => {
currentNote: { currentNote: {
id: null, id: null,
type: '', type: '',
is_default: false,
name: '', name: '',
notes: '', notes: '',
}, },
@@ -24,9 +25,14 @@ export const useNotesStore = (useWindow = false) => {
}, },
actions: { actions: {
getDefaultNoteForType(type) {
return this.notes.find((note) => note.type === type && note.is_default)
},
resetCurrentNote() { resetCurrentNote() {
this.currentNote = { this.currentNote = {
type: '', type: '',
is_default: false,
name: '', name: '',
notes: '', notes: '',
} }

View File

@@ -6,6 +6,7 @@ import { useCompanyStore } from './company'
import { useNotificationStore } from '@/scripts/stores/notification' import { useNotificationStore } from '@/scripts/stores/notification'
import paymentStub from '../stub/payment' import paymentStub from '../stub/payment'
import { handleError } from '@/scripts/helpers/error-handling' import { handleError } from '@/scripts/helpers/error-handling'
import { useNotesStore } from './note'
export const usePaymentStore = (useWindow = false) => { export const usePaymentStore = (useWindow = false) => {
const defineStoreFunc = useWindow ? window.pinia.defineStore : defineStore const defineStoreFunc = useWindow ? window.pinia.defineStore : defineStore
@@ -56,6 +57,7 @@ export const usePaymentStore = (useWindow = false) => {
actions: { actions: {
fetchPaymentInitialData(isEdit) { fetchPaymentInitialData(isEdit) {
const companyStore = useCompanyStore() const companyStore = useCompanyStore()
const notesStore = useNotesStore()
const route = useRoute() const route = useRoute()
this.isFetchingInitialData = true this.isFetchingInitialData = true
@@ -80,6 +82,8 @@ export const usePaymentStore = (useWindow = false) => {
// On Create // On Create
else if (!isEdit && res2.data) { 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_date = moment().format('YYYY-MM-DD')
this.currentPayment.payment_number = res2.data.nextNumber this.currentPayment.payment_number = res2.data.nextNumber
this.currentPayment.currency = this.currentPayment.currency =

View File

@@ -14,6 +14,7 @@ import moment from 'moment'
import _ from 'lodash' import _ from 'lodash'
import { useInvoiceStore } from './invoice' import { useInvoiceStore } from './invoice'
import { useNotificationStore } from '@/scripts/stores/notification' import { useNotificationStore } from '@/scripts/stores/notification'
import { useNotesStore } from './note'
export const useRecurringInvoiceStore = (useWindow = false) => { export const useRecurringInvoiceStore = (useWindow = false) => {
const defineStoreFunc = useWindow ? window.pinia.defineStore : defineStore const defineStoreFunc = useWindow ? window.pinia.defineStore : defineStore
@@ -334,6 +335,7 @@ export const useRecurringInvoiceStore = (useWindow = false) => {
const invoiceStore = useInvoiceStore() const invoiceStore = useInvoiceStore()
const taxTypeStore = useTaxTypeStore() const taxTypeStore = useTaxTypeStore()
const route = useRoute() const route = useRoute()
const notesStore = useNotesStore()
this.isFetchingInitialSettings = true this.isFetchingInitialSettings = true
this.newRecurringInvoice.currency = companyStore.selectedCompanyCurrency this.newRecurringInvoice.currency = companyStore.selectedCompanyCurrency
@@ -348,6 +350,8 @@ export const useRecurringInvoiceStore = (useWindow = false) => {
// on create // on create
if (!isEdit) { if (!isEdit) {
await notesStore.fetchNotes()
this.newRecurringInvoice.notes = notesStore.getDefaultNoteForType('Invoice')?.notes
this.newRecurringInvoice.tax_per_item = this.newRecurringInvoice.tax_per_item =
companyStore.selectedCompanySettings.tax_per_item companyStore.selectedCompanySettings.tax_per_item
this.newRecurringInvoice.discount_per_item = this.newRecurringInvoice.discount_per_item =

View File

@@ -151,6 +151,7 @@ import { cloneDeep } from 'lodash'
import { useInvoiceStore } from '@/scripts/admin/stores/invoice' import { useInvoiceStore } from '@/scripts/admin/stores/invoice'
import { useModuleStore } from '@/scripts/admin/stores/module' import { useModuleStore } from '@/scripts/admin/stores/module'
import { useNotesStore } from '@/scripts/admin/stores/note'
import { useCompanyStore } from '@/scripts/admin/stores/company' import { useCompanyStore } from '@/scripts/admin/stores/company'
import { useCustomFieldStore } from '@/scripts/admin/stores/custom-field' import { useCustomFieldStore } from '@/scripts/admin/stores/custom-field'
@@ -169,6 +170,8 @@ const invoiceStore = useInvoiceStore()
const companyStore = useCompanyStore() const companyStore = useCompanyStore()
const customFieldStore = useCustomFieldStore() const customFieldStore = useCustomFieldStore()
const moduleStore = useModuleStore() const moduleStore = useModuleStore()
const notesStore = useNotesStore()
const { t } = useI18n() const { t } = useI18n()
let route = useRoute() let route = useRoute()
let router = useRouter() let router = useRouter()

View File

@@ -31,6 +31,10 @@
:load-data="refreshTable" :load-data="refreshTable"
/> />
</template> </template>
<template #cell-name="{ row }">
{{ row.data.name }}
<BaseIcon v-if="row.data.is_default" name="StarIcon" class="w-3 h-3 text-primary-400" />
</template>
<template #cell-type="{ row }"> <template #cell-type="{ row }">
{{ getLabelNote(row.data.type) }} {{ getLabelNote(row.data.type) }}
</template> </template>
@@ -42,9 +46,7 @@
import { computed, reactive, ref } from 'vue' import { computed, reactive, ref } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useModalStore } from '@/scripts/stores/modal' import { useModalStore } from '@/scripts/stores/modal'
import { useDialogStore } from '@/scripts/stores/dialog'
import { useNotesStore } from '@/scripts/admin/stores/note' import { useNotesStore } from '@/scripts/admin/stores/note'
import { useNotificationStore } from '@/scripts/stores/notification'
import NoteDropdown from '@/scripts/admin/components/dropdowns/NoteIndexDropdown.vue' import NoteDropdown from '@/scripts/admin/components/dropdowns/NoteIndexDropdown.vue'
import NoteModal from '@/scripts/admin/components/modal-components/NoteModal.vue' import NoteModal from '@/scripts/admin/components/modal-components/NoteModal.vue'
import { useUserStore } from '@/scripts/admin/stores/user' import { useUserStore } from '@/scripts/admin/stores/user'
@@ -53,9 +55,7 @@ import abilities from '@/scripts/admin/stub/abilities'
const { t } = useI18n() const { t } = useI18n()
const modalStore = useModalStore() const modalStore = useModalStore()
const dialogStore = useDialogStore()
const noteStore = useNotesStore() const noteStore = useNotesStore()
const notificationStore = useNotificationStore()
const userStore = useUserStore() const userStore = useUserStore()
const table = ref('') const table = ref('')
@@ -66,7 +66,7 @@ const notesColumns = computed(() => {
key: 'name', key: 'name',
label: t('settings.customization.notes.name'), label: t('settings.customization.notes.name'),
thClass: 'extra', thClass: 'extra',
tdClass: 'font-medium text-gray-900', tdClass: 'font-medium text-gray-900 flex gap-1 items-center',
}, },
{ {
key: 'type', key: 'type',

37
tailwind.config.js vendored
View File

@@ -1,5 +1,5 @@
const colors = require('tailwindcss/colors') import { red, teal, slate } from 'tailwindcss/colors';
const svgToDataUri = require('mini-svg-data-uri') import svgToDataUri from 'mini-svg-data-uri';
function withOpacityValue(cssVariable) { function withOpacityValue(cssVariable) {
return ({ opacityVariable, opacityValue }) => { return ({ opacityVariable, opacityValue }) => {
@@ -13,11 +13,13 @@ function withOpacityValue(cssVariable) {
}; };
} }
module.exports = { export default {
content: [ plugins: [
'./resources/views/**/*.php', require('@tailwindcss/forms'),
'./resources/scripts/**/*.js', require('@tailwindcss/typography'),
'./resources/scripts/**/*.vue', require('@tailwindcss/aspect-ratio'),
require('tailwind-scrollbar'),
require('@rvxlab/tailwind-plugin-ios-full-height')
], ],
theme: { theme: {
extend: { extend: {
@@ -35,9 +37,9 @@ module.exports = {
900: withOpacityValue('--color-primary-900'), 900: withOpacityValue('--color-primary-900'),
}, },
black: '#040405', black: '#040405',
red: colors.red, red: red,
teal: colors.teal, teal: teal,
gray: colors.slate, gray: slate,
}, },
spacing: { spacing: {
88: '22rem', 88: '22rem',
@@ -45,8 +47,8 @@ module.exports = {
backgroundImage: (theme) => ({ backgroundImage: (theme) => ({
'multiselect-caret': `url("${svgToDataUri( 'multiselect-caret': `url("${svgToDataUri(
`<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"> `<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z" clip-rule="evenodd" /> <path fill-rule="evenodd" d="M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z" clip-rule="evenodd" />
</svg>` </svg>`
)}")`, )}")`,
'multiselect-spinner': `url("${svgToDataUri( 'multiselect-spinner': `url("${svgToDataUri(
`<svg viewBox="0 0 512 512" fill="${theme( `<svg viewBox="0 0 512 512" fill="${theme(
@@ -65,12 +67,9 @@ module.exports = {
base: ['Poppins', 'sans-serif'], base: ['Poppins', 'sans-serif'],
}, },
}, },
plugins: [ content: [
require('@tailwindcss/forms'), './resources/views/**/*.php',
require('@tailwindcss/typography'), './resources/scripts/**/*.js',
require('@tailwindcss/aspect-ratio'), './resources/scripts/**/*.vue',
require('tailwind-scrollbar'),
require('@rvxlab/tailwind-plugin-ios-full-height')
], ],
} }