mirror of
https://github.com/InvoiceShelf/InvoiceShelf.git
synced 2026-07-19 23:35:21 +00:00
Now that the legacy v1 frontend (commit 064bdf53) is gone, the v2 directory is the only frontend and the v2 suffix is just noise. Renames resources/scripts-v2 to resources/scripts via git mv (so git records the move as renames, preserving blame and log --follow), then bulk-rewrites the 152 files that imported via @v2/... to use @/scripts/... instead. The existing @ alias (resources/) covers the new path with no extra config needed.
Drops the now-unused @v2 alias from vite.config.js and points the laravel-vite-plugin entry at resources/scripts/main.ts. Updates the only blade reference (resources/views/app.blade.php) to match. The package.json test script (eslint ./resources/scripts) automatically targets the right place after the rename without any edit.
Verified: npm run build exits clean and the Vite warning lines now reference resources/scripts/plugins/i18n.ts, confirming every import resolved through the new path. git log --follow on any moved file walks back through its scripts-v2 history.
573 lines
17 KiB
TypeScript
573 lines
17 KiB
TypeScript
import { defineStore } from 'pinia'
|
|
import { useNotificationStore } from '../../../stores/notification.store'
|
|
import { useCompanyStore } from '../../../stores/company.store'
|
|
import { recurringInvoiceService } from '../../../api/services/recurring-invoice.service'
|
|
import type {
|
|
RecurringInvoiceListParams,
|
|
RecurringInvoiceListResponse,
|
|
FrequencyDateParams,
|
|
} from '../../../api/services/recurring-invoice.service'
|
|
import type {
|
|
RecurringInvoice,
|
|
RecurringInvoiceLimitBy,
|
|
} from '../../../types/domain/recurring-invoice'
|
|
import type { Invoice, InvoiceItem, DiscountType } from '../../../types/domain/invoice'
|
|
import type { Tax, TaxType } from '../../../types/domain/tax'
|
|
import type { Currency } from '../../../types/domain/currency'
|
|
import type { Customer } from '../../../types/domain/customer'
|
|
import type { Note } from '../../../types/domain/note'
|
|
import type { CustomFieldValue } from '../../../types/domain/custom-field'
|
|
import type {
|
|
DocumentTax,
|
|
DocumentItem,
|
|
} from '../../shared/document-form/use-document-calculations'
|
|
import { generateClientId } from '../../../utils'
|
|
|
|
// ----------------------------------------------------------------
|
|
// Frequency options
|
|
// ----------------------------------------------------------------
|
|
|
|
export interface FrequencyOption {
|
|
label: string
|
|
value: string
|
|
}
|
|
|
|
// ----------------------------------------------------------------
|
|
// Stub factories
|
|
// ----------------------------------------------------------------
|
|
|
|
function createTaxStub(): DocumentTax {
|
|
return {
|
|
id: generateClientId(),
|
|
name: '',
|
|
tax_type_id: 0,
|
|
type: 'GENERAL',
|
|
amount: null,
|
|
percent: null,
|
|
compound_tax: false,
|
|
calculation_type: null,
|
|
fixed_amount: 0,
|
|
}
|
|
}
|
|
|
|
function createRecurringInvoiceItemStub(): DocumentItem {
|
|
return {
|
|
id: generateClientId(),
|
|
item_id: null,
|
|
name: '',
|
|
description: null,
|
|
quantity: 1,
|
|
price: 0,
|
|
discount_type: 'fixed',
|
|
discount_val: 0,
|
|
discount: 0,
|
|
total: 0,
|
|
totalTax: 0,
|
|
totalSimpleTax: 0,
|
|
totalCompoundTax: 0,
|
|
tax: 0,
|
|
taxes: [createTaxStub()],
|
|
unit_name: null,
|
|
}
|
|
}
|
|
|
|
export interface RecurringInvoiceFormData {
|
|
id: number | null
|
|
customer: Customer | null
|
|
customer_id: number | null
|
|
template_name: string | null
|
|
starts_at: string
|
|
next_invoice_at: string
|
|
next_invoice_date: string
|
|
frequency: string | null
|
|
selectedFrequency: FrequencyOption | null
|
|
status: string
|
|
limit_by: RecurringInvoiceLimitBy | string
|
|
limit_count: number | null
|
|
limit_date: string | null
|
|
send_automatically: boolean
|
|
notes: string | null
|
|
discount: number
|
|
discount_type: DiscountType
|
|
discount_val: number
|
|
tax: number
|
|
sub_total: number
|
|
total: number
|
|
tax_per_item: string | null
|
|
tax_included: boolean
|
|
sales_tax_type: string | null
|
|
sales_tax_address_type: string | null
|
|
discount_per_item: string | null
|
|
taxes: DocumentTax[]
|
|
items: DocumentItem[]
|
|
customFields: CustomFieldValue[]
|
|
fields: CustomFieldValue[]
|
|
selectedNote: Note | null
|
|
currency: Currency | Record<string, unknown> | null
|
|
currency_id: number | null
|
|
exchange_rate: number | null
|
|
unique_hash?: string
|
|
invoices?: Invoice[]
|
|
}
|
|
|
|
function createRecurringInvoiceStub(): RecurringInvoiceFormData {
|
|
return {
|
|
id: null,
|
|
customer: null,
|
|
customer_id: null,
|
|
template_name: null,
|
|
starts_at: '',
|
|
next_invoice_at: '',
|
|
next_invoice_date: '',
|
|
frequency: null,
|
|
selectedFrequency: null,
|
|
status: 'ACTIVE',
|
|
limit_by: 'NONE',
|
|
limit_count: null,
|
|
limit_date: null,
|
|
send_automatically: false,
|
|
notes: '',
|
|
discount: 0,
|
|
discount_type: 'fixed',
|
|
discount_val: 0,
|
|
tax: 0,
|
|
sub_total: 0,
|
|
total: 0,
|
|
tax_per_item: null,
|
|
tax_included: false,
|
|
sales_tax_type: null,
|
|
sales_tax_address_type: null,
|
|
discount_per_item: null,
|
|
taxes: [],
|
|
items: [createRecurringInvoiceItemStub()],
|
|
customFields: [],
|
|
fields: [],
|
|
selectedNote: null,
|
|
currency: null,
|
|
currency_id: null,
|
|
exchange_rate: null,
|
|
invoices: [],
|
|
}
|
|
}
|
|
|
|
// ----------------------------------------------------------------
|
|
// Store
|
|
// ----------------------------------------------------------------
|
|
|
|
export interface RecurringInvoiceState {
|
|
templates: { name: string; path?: string }[]
|
|
recurringInvoices: RecurringInvoice[]
|
|
selectedRecurringInvoices: number[]
|
|
totalRecurringInvoices: number
|
|
isFetchingInitialSettings: boolean
|
|
isFetchingInvoice: boolean
|
|
isFetchingViewData: boolean
|
|
showExchangeRate: boolean
|
|
selectAllField: boolean
|
|
newRecurringInvoice: RecurringInvoiceFormData
|
|
frequencies: FrequencyOption[]
|
|
}
|
|
|
|
export const useRecurringInvoiceStore = defineStore('recurring-invoice', {
|
|
state: (): RecurringInvoiceState => ({
|
|
templates: [],
|
|
recurringInvoices: [],
|
|
selectedRecurringInvoices: [],
|
|
totalRecurringInvoices: 0,
|
|
isFetchingInitialSettings: false,
|
|
isFetchingInvoice: false,
|
|
isFetchingViewData: false,
|
|
showExchangeRate: false,
|
|
selectAllField: false,
|
|
newRecurringInvoice: createRecurringInvoiceStub(),
|
|
frequencies: [],
|
|
}),
|
|
|
|
getters: {
|
|
getSubTotal(state): number {
|
|
return state.newRecurringInvoice.items.reduce(
|
|
(sum: number, item: DocumentItem) => sum + (item.total ?? 0),
|
|
0,
|
|
)
|
|
},
|
|
|
|
getNetTotal(): number {
|
|
return this.getSubtotalWithDiscount - this.getTotalTax
|
|
},
|
|
|
|
getTotalSimpleTax(state): number {
|
|
return state.newRecurringInvoice.taxes.reduce(
|
|
(sum: number, tax: DocumentTax) => {
|
|
if (!tax.compound_tax) return sum + (tax.amount ?? 0)
|
|
return sum
|
|
},
|
|
0,
|
|
)
|
|
},
|
|
|
|
getTotalCompoundTax(state): number {
|
|
return state.newRecurringInvoice.taxes.reduce(
|
|
(sum: number, tax: DocumentTax) => {
|
|
if (tax.compound_tax) return sum + (tax.amount ?? 0)
|
|
return sum
|
|
},
|
|
0,
|
|
)
|
|
},
|
|
|
|
getTotalTax(): number {
|
|
if (
|
|
this.newRecurringInvoice.tax_per_item === 'NO' ||
|
|
this.newRecurringInvoice.tax_per_item === null
|
|
) {
|
|
return this.getTotalSimpleTax + this.getTotalCompoundTax
|
|
}
|
|
return this.newRecurringInvoice.items.reduce(
|
|
(sum: number, item: DocumentItem) => sum + (item.tax ?? 0),
|
|
0,
|
|
)
|
|
},
|
|
|
|
getSubtotalWithDiscount(): number {
|
|
return this.getSubTotal - this.newRecurringInvoice.discount_val
|
|
},
|
|
|
|
getTotal(): number {
|
|
if (this.newRecurringInvoice.tax_included) {
|
|
return this.getSubtotalWithDiscount
|
|
}
|
|
return this.getSubtotalWithDiscount + this.getTotalTax
|
|
},
|
|
},
|
|
|
|
actions: {
|
|
initFrequencies(t: (key: string) => string): void {
|
|
this.frequencies = [
|
|
// Common business intervals
|
|
{ label: t('recurring_invoices.frequency.every_week'), value: '0 0 * * 0' },
|
|
{ label: t('recurring_invoices.frequency.every_2_weeks'), value: '0 0 */14 * *' },
|
|
{ label: t('recurring_invoices.frequency.every_month'), value: '0 0 1 * *' },
|
|
{ label: t('recurring_invoices.frequency.every_2_months'), value: '0 0 1 */2 *' },
|
|
{ label: t('recurring_invoices.frequency.every_quarter'), value: '0 0 1 */3 *' },
|
|
{ label: t('recurring_invoices.frequency.every_6_month'), value: '0 0 1 */6 *' },
|
|
{ label: t('recurring_invoices.frequency.every_year'), value: '0 0 1 1 *' },
|
|
// Less common intervals
|
|
{ label: t('recurring_invoices.frequency.every_day'), value: '0 0 * * *' },
|
|
{ label: t('recurring_invoices.frequency.every_15_days_at_midnight'), value: '0 5 */15 * *' },
|
|
{ label: t('recurring_invoices.frequency.every_hour'), value: '0 * * * *' },
|
|
{ label: t('recurring_invoices.frequency.every_minute'), value: '* * * * *' },
|
|
// Custom cron expression
|
|
{ label: t('recurring_invoices.frequency.custom'), value: 'CUSTOM' },
|
|
]
|
|
},
|
|
|
|
resetCurrentRecurringInvoice(): void {
|
|
this.newRecurringInvoice = createRecurringInvoiceStub()
|
|
},
|
|
|
|
deselectItem(index: number): void {
|
|
this.newRecurringInvoice.items[index] = {
|
|
...createRecurringInvoiceItemStub(),
|
|
taxes: [createTaxStub()],
|
|
}
|
|
},
|
|
|
|
addItem(): void {
|
|
this.newRecurringInvoice.items.push({
|
|
...createRecurringInvoiceItemStub(),
|
|
taxes: [createTaxStub()],
|
|
})
|
|
},
|
|
|
|
removeItem(index: number): void {
|
|
this.newRecurringInvoice.items.splice(index, 1)
|
|
},
|
|
|
|
updateItem(data: DocumentItem & { index: number }): void {
|
|
Object.assign(this.newRecurringInvoice.items[data.index], { ...data })
|
|
},
|
|
|
|
setTemplate(name: string): void {
|
|
this.newRecurringInvoice.template_name = name
|
|
},
|
|
|
|
setSelectedFrequency(): void {
|
|
const found = this.frequencies.find(
|
|
(f) => f.value === this.newRecurringInvoice.frequency,
|
|
)
|
|
if (found) {
|
|
this.newRecurringInvoice.selectedFrequency = found
|
|
} else {
|
|
this.newRecurringInvoice.selectedFrequency = {
|
|
label: 'Custom',
|
|
value: 'CUSTOM',
|
|
}
|
|
}
|
|
},
|
|
|
|
resetSelectedNote(): void {
|
|
this.newRecurringInvoice.selectedNote = null
|
|
},
|
|
|
|
selectNote(data: Note): void {
|
|
this.newRecurringInvoice.selectedNote = null
|
|
this.newRecurringInvoice.selectedNote = data
|
|
},
|
|
|
|
resetSelectedCustomer(): void {
|
|
this.newRecurringInvoice.customer = null
|
|
this.newRecurringInvoice.customer_id = null
|
|
},
|
|
|
|
async selectCustomer(id: number): Promise<unknown> {
|
|
const { customerService } = await import(
|
|
'../../../api/services/customer.service'
|
|
)
|
|
const response = await customerService.get(id)
|
|
this.newRecurringInvoice.customer =
|
|
response.data as unknown as Customer
|
|
this.newRecurringInvoice.customer_id = response.data.id
|
|
return response
|
|
},
|
|
|
|
async fetchRecurringInvoices(
|
|
params: RecurringInvoiceListParams & {
|
|
from_date?: string
|
|
to_date?: string
|
|
},
|
|
): Promise<{ data: RecurringInvoiceListResponse }> {
|
|
const response = await recurringInvoiceService.list(params)
|
|
this.recurringInvoices = response.data
|
|
this.totalRecurringInvoices =
|
|
response.meta.recurring_invoice_total_count
|
|
return { data: response }
|
|
},
|
|
|
|
async fetchRecurringInvoice(
|
|
id: number,
|
|
): Promise<{ data: { data: RecurringInvoice } }> {
|
|
this.isFetchingViewData = true
|
|
try {
|
|
const response = await recurringInvoiceService.get(id)
|
|
Object.assign(this.newRecurringInvoice, response.data)
|
|
this.newRecurringInvoice.invoices = response.data.invoices ?? []
|
|
this.setSelectedFrequency()
|
|
this.isFetchingViewData = false
|
|
return { data: response }
|
|
} catch (err) {
|
|
this.isFetchingViewData = false
|
|
throw err
|
|
}
|
|
},
|
|
|
|
async addRecurringInvoice(
|
|
data: Record<string, unknown>,
|
|
): Promise<{ data: { data: RecurringInvoice } }> {
|
|
const response = await recurringInvoiceService.create(data as never)
|
|
this.recurringInvoices = [
|
|
...this.recurringInvoices,
|
|
response.data,
|
|
]
|
|
|
|
const notificationStore = useNotificationStore()
|
|
notificationStore.showNotification({
|
|
type: 'success',
|
|
message: 'recurring_invoices.created_message',
|
|
})
|
|
|
|
return { data: response }
|
|
},
|
|
|
|
async updateRecurringInvoice(
|
|
data: Record<string, unknown>,
|
|
): Promise<{ data: { data: RecurringInvoice } }> {
|
|
const response = await recurringInvoiceService.update(
|
|
data.id as number,
|
|
data as never,
|
|
)
|
|
const pos = this.recurringInvoices.findIndex(
|
|
(inv) => inv.id === response.data.id,
|
|
)
|
|
if (pos !== -1) {
|
|
this.recurringInvoices[pos] = response.data
|
|
}
|
|
|
|
const notificationStore = useNotificationStore()
|
|
notificationStore.showNotification({
|
|
type: 'success',
|
|
message: 'recurring_invoices.updated_message',
|
|
})
|
|
|
|
return { data: response }
|
|
},
|
|
|
|
async deleteRecurringInvoice(
|
|
payload: { ids: number[] },
|
|
): Promise<{ data: { success: boolean } }> {
|
|
const response = await recurringInvoiceService.delete(payload)
|
|
const id = payload.ids[0]
|
|
const index = this.recurringInvoices.findIndex((inv) => inv.id === id)
|
|
if (index !== -1) {
|
|
this.recurringInvoices.splice(index, 1)
|
|
}
|
|
return { data: response }
|
|
},
|
|
|
|
async deleteMultipleRecurringInvoices(
|
|
singleId?: number | null,
|
|
): Promise<{ data: { success: boolean } }> {
|
|
const ids = singleId
|
|
? [singleId]
|
|
: this.selectedRecurringInvoices
|
|
const response = await recurringInvoiceService.delete({ ids })
|
|
this.selectedRecurringInvoices.forEach((invoiceId) => {
|
|
const index = this.recurringInvoices.findIndex(
|
|
(inv) => inv.id === invoiceId,
|
|
)
|
|
if (index !== -1) {
|
|
this.recurringInvoices.splice(index, 1)
|
|
}
|
|
})
|
|
this.selectedRecurringInvoices = []
|
|
return { data: response }
|
|
},
|
|
|
|
async fetchRecurringInvoiceFrequencyDate(
|
|
params: FrequencyDateParams,
|
|
): Promise<void> {
|
|
const response =
|
|
await recurringInvoiceService.getFrequencyDate(params)
|
|
this.newRecurringInvoice.next_invoice_at =
|
|
response.next_invoice_at
|
|
},
|
|
|
|
selectRecurringInvoice(data: number[]): void {
|
|
this.selectedRecurringInvoices = data
|
|
this.selectAllField =
|
|
this.selectedRecurringInvoices.length ===
|
|
this.recurringInvoices.length
|
|
},
|
|
|
|
selectAllRecurringInvoices(): void {
|
|
if (
|
|
this.selectedRecurringInvoices.length ===
|
|
this.recurringInvoices.length
|
|
) {
|
|
this.selectedRecurringInvoices = []
|
|
this.selectAllField = false
|
|
} else {
|
|
this.selectedRecurringInvoices = this.recurringInvoices.map(
|
|
(inv) => inv.id,
|
|
)
|
|
this.selectAllField = true
|
|
}
|
|
},
|
|
|
|
addSalesTaxUs(taxTypes: TaxType[]): void {
|
|
const salesTax = createTaxStub()
|
|
const found = this.newRecurringInvoice.taxes.find(
|
|
(t) => t.name === 'Sales Tax' && t.type === 'MODULE',
|
|
)
|
|
if (found) {
|
|
for (const key in found) {
|
|
if (Object.prototype.hasOwnProperty.call(salesTax, key)) {
|
|
;(salesTax as Record<string, unknown>)[key] = (
|
|
found as Record<string, unknown>
|
|
)[key]
|
|
}
|
|
}
|
|
salesTax.id = found.tax_type_id
|
|
taxTypes.push(salesTax as unknown as TaxType)
|
|
}
|
|
},
|
|
|
|
async fetchRecurringInvoiceInitialSettings(
|
|
isEdit: boolean,
|
|
routeParams?: { id?: string; query?: Record<string, string> },
|
|
companySettingsParam?: Record<string, string>,
|
|
companyCurrency?: Currency,
|
|
): Promise<void> {
|
|
this.isFetchingInitialSettings = true
|
|
|
|
const companyStore = useCompanyStore()
|
|
const companySettings = companySettingsParam ?? companyStore.selectedCompanySettings
|
|
|
|
if (companyCurrency || companyStore.selectedCompanyCurrency) {
|
|
this.newRecurringInvoice.currency = companyCurrency ?? companyStore.selectedCompanyCurrency!
|
|
}
|
|
|
|
if (routeParams?.query?.customer) {
|
|
try {
|
|
await this.selectCustomer(
|
|
Number(routeParams.query.customer),
|
|
)
|
|
} catch {
|
|
// Silently fail
|
|
}
|
|
}
|
|
|
|
const editActions: Promise<unknown>[] = []
|
|
|
|
if (!isEdit && companySettings) {
|
|
this.newRecurringInvoice.tax_per_item =
|
|
companySettings.tax_per_item ?? null
|
|
this.newRecurringInvoice.discount_per_item =
|
|
companySettings.discount_per_item ?? null
|
|
this.newRecurringInvoice.sales_tax_type =
|
|
companySettings.sales_tax_type ?? null
|
|
this.newRecurringInvoice.sales_tax_address_type =
|
|
companySettings.sales_tax_address_type ?? null
|
|
this.newRecurringInvoice.starts_at = formatDate(new Date())
|
|
this.newRecurringInvoice.next_invoice_date = formatDate(
|
|
addDays(new Date(), 7),
|
|
)
|
|
} else if (isEdit && routeParams?.id) {
|
|
editActions.push(
|
|
this.fetchRecurringInvoice(Number(routeParams.id)),
|
|
)
|
|
}
|
|
|
|
try {
|
|
const [, , , , editRes] = await Promise.all([
|
|
Promise.resolve(), // placeholder for items fetch
|
|
this.resetSelectedNote(),
|
|
Promise.resolve(), // placeholder for invoice templates
|
|
Promise.resolve(), // placeholder for tax types fetch
|
|
...editActions,
|
|
])
|
|
|
|
if (!isEdit) {
|
|
if (this.templates.length) {
|
|
this.setTemplate(this.templates[0].name)
|
|
}
|
|
} else if (editRes) {
|
|
const res = editRes as { data: { data: RecurringInvoice } }
|
|
if (res?.data?.data?.template_name) {
|
|
this.setTemplate(res.data.data.template_name)
|
|
}
|
|
this.addSalesTaxUs([])
|
|
}
|
|
} catch {
|
|
// Error handling
|
|
} finally {
|
|
this.isFetchingInitialSettings = false
|
|
}
|
|
},
|
|
},
|
|
})
|
|
|
|
function formatDate(date: Date): string {
|
|
const year = date.getFullYear()
|
|
const month = String(date.getMonth() + 1).padStart(2, '0')
|
|
const day = String(date.getDate()).padStart(2, '0')
|
|
return `${year}-${month}-${day}`
|
|
}
|
|
|
|
function addDays(date: Date, days: number): Date {
|
|
const result = new Date(date)
|
|
result.setDate(result.getDate() + days)
|
|
return result
|
|
}
|
|
|
|
export type RecurringInvoiceStore = ReturnType<typeof useRecurringInvoiceStore>
|