mirror of
https://github.com/InvoiceShelf/InvoiceShelf.git
synced 2026-04-19 03:04:05 +00:00
Phase 1: TypeScript foundation in scripts-v2/
Create the complete TypeScript foundation for the Vue 3 migration in a parallel scripts-v2/ directory. 72 files, 5430 lines, zero any types, strict mode. - types/ (21 files): Domain interfaces for all 17 entities derived from actual Laravel models and API resources. Enums for all statuses. Generic API response wrappers. - api/ (29 files): Typed axios client with interceptors, endpoint constants from routes/api.php, 25 typed service classes covering every API endpoint. - composables/ (14 files): Vue 3 composition functions for auth, notifications, dialogs, modals, pagination, filters, currency, dates, theme, sidebar, company context, and permissions. - utils/ (5 files): Pure typed utilities for money formatting, date formatting (date-fns), localStorage, and error handling. - config/ (3 files): Typed ability constants, app constants. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
28
resources/scripts-v2/api/client.ts
Normal file
28
resources/scripts-v2/api/client.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import axios, { type AxiosInstance, type InternalAxiosRequestConfig } from 'axios'
|
||||
|
||||
const client: AxiosInstance = axios.create({
|
||||
withCredentials: true,
|
||||
headers: {
|
||||
common: {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
client.interceptors.request.use((config: InternalAxiosRequestConfig) => {
|
||||
const companyId = localStorage.getItem('selectedCompany')
|
||||
const authToken = localStorage.getItem('auth.token')
|
||||
const isAdminMode = localStorage.getItem('isAdminMode') === 'true'
|
||||
|
||||
if (authToken) {
|
||||
config.headers.Authorization = authToken
|
||||
}
|
||||
|
||||
if (companyId && !isAdminMode) {
|
||||
config.headers.company = companyId
|
||||
}
|
||||
|
||||
return config
|
||||
})
|
||||
|
||||
export { client }
|
||||
164
resources/scripts-v2/api/endpoints.ts
Normal file
164
resources/scripts-v2/api/endpoints.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
export const API = {
|
||||
// Authentication & Password Reset
|
||||
LOGIN: '/api/v1/auth/login',
|
||||
LOGOUT: '/api/v1/auth/logout',
|
||||
FORGOT_PASSWORD: '/api/v1/auth/password/email',
|
||||
RESET_PASSWORD: '/api/v1/auth/reset/password',
|
||||
AUTH_CHECK: '/api/v1/auth/check',
|
||||
CSRF_COOKIE: '/sanctum/csrf-cookie',
|
||||
REGISTER_WITH_INVITATION: '/api/v1/auth/register-with-invitation',
|
||||
|
||||
// Invitation Registration (public)
|
||||
INVITATION_DETAILS: '/api/v1/invitations', // append /{token}/details
|
||||
|
||||
// Invitations (user-scoped)
|
||||
INVITATIONS_PENDING: '/api/v1/invitations/pending',
|
||||
INVITATIONS: '/api/v1/invitations', // append /{token}/accept or /{token}/decline
|
||||
|
||||
// Bootstrap & General
|
||||
BOOTSTRAP: '/api/v1/bootstrap',
|
||||
CONFIG: '/api/v1/config',
|
||||
CURRENT_COMPANY: '/api/v1/current-company',
|
||||
SEARCH: '/api/v1/search',
|
||||
SEARCH_USERS: '/api/v1/search/user',
|
||||
APP_VERSION: '/api/v1/app/version',
|
||||
COUNTRIES: '/api/v1/countries',
|
||||
|
||||
// Dashboard
|
||||
DASHBOARD: '/api/v1/dashboard',
|
||||
|
||||
// Customers
|
||||
CUSTOMERS: '/api/v1/customers',
|
||||
CUSTOMERS_DELETE: '/api/v1/customers/delete',
|
||||
CUSTOMER_STATS: '/api/v1/customers', // append /{id}/stats
|
||||
|
||||
// Items & Units
|
||||
ITEMS: '/api/v1/items',
|
||||
ITEMS_DELETE: '/api/v1/items/delete',
|
||||
UNITS: '/api/v1/units',
|
||||
|
||||
// Invoices
|
||||
INVOICES: '/api/v1/invoices',
|
||||
INVOICES_DELETE: '/api/v1/invoices/delete',
|
||||
INVOICE_TEMPLATES: '/api/v1/invoices/templates',
|
||||
|
||||
// Recurring Invoices
|
||||
RECURRING_INVOICES: '/api/v1/recurring-invoices',
|
||||
RECURRING_INVOICES_DELETE: '/api/v1/recurring-invoices/delete',
|
||||
RECURRING_INVOICE_FREQUENCY: '/api/v1/recurring-invoice-frequency',
|
||||
|
||||
// Estimates
|
||||
ESTIMATES: '/api/v1/estimates',
|
||||
ESTIMATES_DELETE: '/api/v1/estimates/delete',
|
||||
ESTIMATE_TEMPLATES: '/api/v1/estimates/templates',
|
||||
|
||||
// Expenses
|
||||
EXPENSES: '/api/v1/expenses',
|
||||
EXPENSES_DELETE: '/api/v1/expenses/delete',
|
||||
|
||||
// Expense Categories
|
||||
CATEGORIES: '/api/v1/categories',
|
||||
|
||||
// Payments
|
||||
PAYMENTS: '/api/v1/payments',
|
||||
PAYMENTS_DELETE: '/api/v1/payments/delete',
|
||||
PAYMENT_METHODS: '/api/v1/payment-methods',
|
||||
|
||||
// Custom Fields
|
||||
CUSTOM_FIELDS: '/api/v1/custom-fields',
|
||||
|
||||
// Notes
|
||||
NOTES: '/api/v1/notes',
|
||||
|
||||
// Tax Types
|
||||
TAX_TYPES: '/api/v1/tax-types',
|
||||
|
||||
// Roles & Abilities
|
||||
ROLES: '/api/v1/roles',
|
||||
ABILITIES: '/api/v1/abilities',
|
||||
|
||||
// Company
|
||||
COMPANY: '/api/v1/company',
|
||||
COMPANY_UPLOAD_LOGO: '/api/v1/company/upload-logo',
|
||||
COMPANY_SETTINGS: '/api/v1/company/settings',
|
||||
COMPANY_HAS_TRANSACTIONS: '/api/v1/company/has-transactions',
|
||||
COMPANIES: '/api/v1/companies',
|
||||
COMPANIES_DELETE: '/api/v1/companies/delete',
|
||||
TRANSFER_OWNERSHIP: '/api/v1/transfer/ownership', // append /{userId}
|
||||
|
||||
// Company Invitations (company-scoped)
|
||||
COMPANY_INVITATIONS: '/api/v1/company-invitations',
|
||||
|
||||
// Members
|
||||
MEMBERS: '/api/v1/members',
|
||||
MEMBERS_DELETE: '/api/v1/members/delete',
|
||||
|
||||
// User Profile & Settings
|
||||
ME: '/api/v1/me',
|
||||
ME_SETTINGS: '/api/v1/me/settings',
|
||||
ME_UPLOAD_AVATAR: '/api/v1/me/upload-avatar',
|
||||
|
||||
// Global Settings (admin)
|
||||
SETTINGS: '/api/v1/settings',
|
||||
|
||||
// Mail Configuration (global)
|
||||
MAIL_DRIVERS: '/api/v1/mail/drivers',
|
||||
MAIL_CONFIG: '/api/v1/mail/config',
|
||||
MAIL_TEST: '/api/v1/mail/test',
|
||||
|
||||
// Company Mail Configuration
|
||||
COMPANY_MAIL_DEFAULT_CONFIG: '/api/v1/company/mail/config',
|
||||
COMPANY_MAIL_CONFIG: '/api/v1/company/mail/company-config',
|
||||
COMPANY_MAIL_TEST: '/api/v1/company/mail/company-test',
|
||||
|
||||
// PDF Configuration
|
||||
PDF_DRIVERS: '/api/v1/pdf/drivers',
|
||||
PDF_CONFIG: '/api/v1/pdf/config',
|
||||
|
||||
// Disks & Backups
|
||||
DISKS: '/api/v1/disks',
|
||||
DISK_DRIVERS: '/api/v1/disk/drivers',
|
||||
BACKUPS: '/api/v1/backups',
|
||||
DOWNLOAD_BACKUP: '/api/v1/download-backup',
|
||||
|
||||
// Exchange Rates & Currencies
|
||||
CURRENCIES: '/api/v1/currencies',
|
||||
CURRENCIES_USED: '/api/v1/currencies/used',
|
||||
CURRENCIES_BULK_UPDATE: '/api/v1/currencies/bulk-update-exchange-rate',
|
||||
EXCHANGE_RATE_PROVIDERS: '/api/v1/exchange-rate-providers',
|
||||
USED_CURRENCIES: '/api/v1/used-currencies',
|
||||
SUPPORTED_CURRENCIES: '/api/v1/supported-currencies',
|
||||
|
||||
// Serial Numbers
|
||||
NEXT_NUMBER: '/api/v1/next-number',
|
||||
NUMBER_PLACEHOLDERS: '/api/v1/number-placeholders',
|
||||
|
||||
// Formats
|
||||
TIMEZONES: '/api/v1/timezones',
|
||||
DATE_FORMATS: '/api/v1/date/formats',
|
||||
TIME_FORMATS: '/api/v1/time/formats',
|
||||
|
||||
// Modules
|
||||
MODULES: '/api/v1/modules',
|
||||
MODULES_CHECK: '/api/v1/modules/check',
|
||||
MODULES_DOWNLOAD: '/api/v1/modules/download',
|
||||
MODULES_UPLOAD: '/api/v1/modules/upload',
|
||||
MODULES_UNZIP: '/api/v1/modules/unzip',
|
||||
MODULES_COPY: '/api/v1/modules/copy',
|
||||
MODULES_COMPLETE: '/api/v1/modules/complete',
|
||||
|
||||
// Self Update
|
||||
CHECK_UPDATE: '/api/v1/check/update',
|
||||
UPDATE_DOWNLOAD: '/api/v1/update/download',
|
||||
UPDATE_UNZIP: '/api/v1/update/unzip',
|
||||
UPDATE_COPY: '/api/v1/update/copy',
|
||||
UPDATE_DELETE: '/api/v1/update/delete',
|
||||
UPDATE_MIGRATE: '/api/v1/update/migrate',
|
||||
UPDATE_FINISH: '/api/v1/update/finish',
|
||||
|
||||
// Super Admin
|
||||
SUPER_ADMIN_DASHBOARD: '/api/v1/super-admin/dashboard',
|
||||
SUPER_ADMIN_COMPANIES: '/api/v1/super-admin/companies',
|
||||
SUPER_ADMIN_USERS: '/api/v1/super-admin/users',
|
||||
SUPER_ADMIN_STOP_IMPERSONATING: '/api/v1/super-admin/stop-impersonating',
|
||||
} as const
|
||||
123
resources/scripts-v2/api/index.ts
Normal file
123
resources/scripts-v2/api/index.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
export { client } from './client'
|
||||
export { API } from './endpoints'
|
||||
|
||||
export {
|
||||
authService,
|
||||
bootstrapService,
|
||||
invoiceService,
|
||||
estimateService,
|
||||
recurringInvoiceService,
|
||||
customerService,
|
||||
paymentService,
|
||||
expenseService,
|
||||
itemService,
|
||||
companyService,
|
||||
userService,
|
||||
memberService,
|
||||
settingService,
|
||||
dashboardService,
|
||||
reportService,
|
||||
roleService,
|
||||
taxTypeService,
|
||||
customFieldService,
|
||||
noteService,
|
||||
exchangeRateService,
|
||||
moduleService,
|
||||
backupService,
|
||||
mailService,
|
||||
pdfService,
|
||||
diskService,
|
||||
} from './services'
|
||||
|
||||
// Re-export all service types
|
||||
export type {
|
||||
LoginPayload,
|
||||
LoginResponse,
|
||||
ForgotPasswordPayload,
|
||||
ResetPasswordPayload,
|
||||
RegisterWithInvitationPayload,
|
||||
BootstrapResponse,
|
||||
MenuItem,
|
||||
CurrentCompanyResponse,
|
||||
InvoiceListParams,
|
||||
InvoiceListResponse,
|
||||
SendInvoicePayload,
|
||||
InvoiceStatusPayload,
|
||||
InvoiceTemplatesResponse,
|
||||
EstimateListParams,
|
||||
EstimateListResponse,
|
||||
SendEstimatePayload,
|
||||
EstimateStatusPayload,
|
||||
EstimateTemplatesResponse,
|
||||
RecurringInvoiceListParams,
|
||||
RecurringInvoiceListResponse,
|
||||
FrequencyDateParams,
|
||||
FrequencyDateResponse,
|
||||
CustomerListParams,
|
||||
CustomerListResponse,
|
||||
CustomerStatsData,
|
||||
PaymentListParams,
|
||||
PaymentListResponse,
|
||||
SendPaymentPayload,
|
||||
CreatePaymentMethodPayload,
|
||||
ExpenseListParams,
|
||||
ExpenseListResponse,
|
||||
CreateExpenseCategoryPayload,
|
||||
ItemListParams,
|
||||
ItemListResponse,
|
||||
CreateItemPayload,
|
||||
CreateUnitPayload,
|
||||
UpdateCompanyPayload,
|
||||
CompanySettingsPayload,
|
||||
CreateCompanyPayload,
|
||||
UpdateProfilePayload,
|
||||
UserSettingsPayload,
|
||||
MemberListParams,
|
||||
MemberListResponse,
|
||||
UpdateMemberPayload,
|
||||
InviteMemberPayload,
|
||||
DeleteMembersPayload,
|
||||
ConfigResponse,
|
||||
GlobalSettingsPayload,
|
||||
DateFormat,
|
||||
TimeFormat,
|
||||
DashboardParams,
|
||||
DashboardResponse,
|
||||
ChartData,
|
||||
ReportParams,
|
||||
SalesReportResponse,
|
||||
ProfitLossReportResponse,
|
||||
ExpenseReportResponse,
|
||||
TaxReportResponse,
|
||||
CreateRolePayload,
|
||||
AbilitiesResponse,
|
||||
CreateTaxTypePayload,
|
||||
CustomFieldListParams,
|
||||
CreateCustomFieldPayload,
|
||||
CreateNotePayload,
|
||||
CreateExchangeRateProviderPayload,
|
||||
BulkUpdatePayload,
|
||||
ExchangeRateResponse,
|
||||
ActiveProviderResponse,
|
||||
Module,
|
||||
ModuleInstallPayload,
|
||||
ModuleCheckResponse,
|
||||
Backup,
|
||||
CreateBackupPayload,
|
||||
DeleteBackupParams,
|
||||
MailConfig,
|
||||
MailConfigResponse,
|
||||
MailDriver,
|
||||
SmtpConfig,
|
||||
MailgunConfig,
|
||||
SesConfig,
|
||||
TestMailPayload,
|
||||
PdfConfig,
|
||||
PdfConfigResponse,
|
||||
PdfDriver,
|
||||
DomPdfConfig,
|
||||
GotenbergConfig,
|
||||
Disk,
|
||||
DiskDriversResponse,
|
||||
CreateDiskPayload,
|
||||
} from './services'
|
||||
81
resources/scripts-v2/api/services/auth.service.ts
Normal file
81
resources/scripts-v2/api/services/auth.service.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { client } from '../client'
|
||||
import { API } from '../endpoints'
|
||||
import type { User } from '../../types/domain/user'
|
||||
import type { ApiResponse } from '../../types/api'
|
||||
|
||||
export interface LoginPayload {
|
||||
email: string
|
||||
password: string
|
||||
remember?: boolean
|
||||
}
|
||||
|
||||
export interface LoginResponse {
|
||||
token: string
|
||||
user: User
|
||||
}
|
||||
|
||||
export interface ForgotPasswordPayload {
|
||||
email: string
|
||||
}
|
||||
|
||||
export interface ResetPasswordPayload {
|
||||
email: string
|
||||
password: string
|
||||
password_confirmation: string
|
||||
token: string
|
||||
}
|
||||
|
||||
export interface InvitationDetails {
|
||||
email: string
|
||||
company_name: string
|
||||
invited_by: string
|
||||
}
|
||||
|
||||
export interface RegisterWithInvitationPayload {
|
||||
name: string
|
||||
email: string
|
||||
password: string
|
||||
password_confirmation: string
|
||||
token: string
|
||||
}
|
||||
|
||||
export const authService = {
|
||||
async refreshCsrfCookie(): Promise<void> {
|
||||
await client.get(API.CSRF_COOKIE)
|
||||
},
|
||||
|
||||
async login(payload: LoginPayload): Promise<ApiResponse<LoginResponse>> {
|
||||
await client.get(API.CSRF_COOKIE)
|
||||
const { data } = await client.post(API.LOGIN, payload)
|
||||
return data
|
||||
},
|
||||
|
||||
async logout(): Promise<void> {
|
||||
await client.post(API.LOGOUT)
|
||||
},
|
||||
|
||||
async forgotPassword(payload: ForgotPasswordPayload): Promise<ApiResponse<{ success: boolean }>> {
|
||||
const { data } = await client.post(API.FORGOT_PASSWORD, payload)
|
||||
return data
|
||||
},
|
||||
|
||||
async resetPassword(payload: ResetPasswordPayload): Promise<ApiResponse<{ success: boolean }>> {
|
||||
const { data } = await client.post(API.RESET_PASSWORD, payload)
|
||||
return data
|
||||
},
|
||||
|
||||
async check(): Promise<ApiResponse<{ success: boolean }>> {
|
||||
const { data } = await client.get(API.AUTH_CHECK)
|
||||
return data
|
||||
},
|
||||
|
||||
async getInvitationDetails(token: string): Promise<ApiResponse<InvitationDetails>> {
|
||||
const { data } = await client.get(`${API.INVITATION_DETAILS}/${token}/details`)
|
||||
return data
|
||||
},
|
||||
|
||||
async registerWithInvitation(payload: RegisterWithInvitationPayload): Promise<ApiResponse<User>> {
|
||||
const { data } = await client.post(API.REGISTER_WITH_INVITATION, payload)
|
||||
return data
|
||||
},
|
||||
}
|
||||
47
resources/scripts-v2/api/services/backup.service.ts
Normal file
47
resources/scripts-v2/api/services/backup.service.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { client } from '../client'
|
||||
import { API } from '../endpoints'
|
||||
import type { ApiResponse, ListParams } from '../../types/api'
|
||||
|
||||
export interface Backup {
|
||||
id: number
|
||||
disk: string
|
||||
path: string
|
||||
created_at: string
|
||||
file_size: string
|
||||
}
|
||||
|
||||
export interface CreateBackupPayload {
|
||||
option: 'full' | 'database' | 'files'
|
||||
selected_disk: string | null
|
||||
}
|
||||
|
||||
export interface DeleteBackupParams {
|
||||
disk: string
|
||||
path?: string
|
||||
file_name?: string
|
||||
}
|
||||
|
||||
export const backupService = {
|
||||
async list(params?: ListParams): Promise<ApiResponse<Backup[]>> {
|
||||
const { data } = await client.get(API.BACKUPS, { params })
|
||||
return data
|
||||
},
|
||||
|
||||
async create(payload: CreateBackupPayload): Promise<{ success: boolean }> {
|
||||
const { data } = await client.post(API.BACKUPS, payload)
|
||||
return data
|
||||
},
|
||||
|
||||
async delete(params: DeleteBackupParams): Promise<{ success: boolean }> {
|
||||
const { data } = await client.delete(`${API.BACKUPS}/${params.disk}`, { params })
|
||||
return data
|
||||
},
|
||||
|
||||
async download(params: { disk: string; path?: string; file_name?: string }): Promise<Blob> {
|
||||
const { data } = await client.get(API.DOWNLOAD_BACKUP, {
|
||||
params,
|
||||
responseType: 'blob',
|
||||
})
|
||||
return data
|
||||
},
|
||||
}
|
||||
53
resources/scripts-v2/api/services/bootstrap.service.ts
Normal file
53
resources/scripts-v2/api/services/bootstrap.service.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { client } from '../client'
|
||||
import { API } from '../endpoints'
|
||||
import type { User, UserSetting } from '../../types/domain/user'
|
||||
import type { Company } from '../../types/domain/company'
|
||||
import type { Currency } from '../../types/domain/currency'
|
||||
import type { Ability } from '../../types/domain/role'
|
||||
|
||||
export interface MenuItem {
|
||||
title: string
|
||||
name: string
|
||||
route: string
|
||||
icon: string
|
||||
group: string
|
||||
ability?: string
|
||||
}
|
||||
|
||||
export interface BootstrapResponse {
|
||||
current_user: User
|
||||
current_user_settings: Record<string, string>
|
||||
current_user_abilities: Ability[]
|
||||
companies: Company[]
|
||||
current_company: Company | null
|
||||
current_company_settings: Record<string, string>
|
||||
current_company_currency: Currency | null
|
||||
main_menu: MenuItem[]
|
||||
setting_menu: MenuItem[]
|
||||
config: Record<string, unknown>
|
||||
global_settings: Record<string, string>
|
||||
modules: string[]
|
||||
pending_invitations?: Array<{
|
||||
token: string
|
||||
company_name: string
|
||||
invited_by: string
|
||||
email: string
|
||||
}>
|
||||
}
|
||||
|
||||
export interface CurrentCompanyResponse {
|
||||
data: Company
|
||||
}
|
||||
|
||||
export const bootstrapService = {
|
||||
async bootstrap(adminMode?: boolean): Promise<BootstrapResponse> {
|
||||
const url = adminMode ? `${API.BOOTSTRAP}?admin_mode=1` : API.BOOTSTRAP
|
||||
const { data } = await client.get(url)
|
||||
return data
|
||||
},
|
||||
|
||||
async getCurrentCompany(): Promise<CurrentCompanyResponse> {
|
||||
const { data } = await client.get(API.CURRENT_COMPANY)
|
||||
return data
|
||||
},
|
||||
}
|
||||
100
resources/scripts-v2/api/services/company.service.ts
Normal file
100
resources/scripts-v2/api/services/company.service.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import { client } from '../client'
|
||||
import { API } from '../endpoints'
|
||||
import type { Company } from '../../types/domain/company'
|
||||
import type { ApiResponse } from '../../types/api'
|
||||
|
||||
export interface UpdateCompanyPayload {
|
||||
name: string
|
||||
vat_id?: string | null
|
||||
tax_id?: string | null
|
||||
phone?: string | null
|
||||
address?: {
|
||||
address_street_1?: string | null
|
||||
address_street_2?: string | null
|
||||
city?: string | null
|
||||
state?: string | null
|
||||
country_id?: number | null
|
||||
zip?: string | null
|
||||
phone?: string | null
|
||||
}
|
||||
}
|
||||
|
||||
export interface CompanySettingsPayload {
|
||||
settings: Record<string, string | number | boolean | null>
|
||||
}
|
||||
|
||||
export interface CreateCompanyPayload {
|
||||
name: string
|
||||
currency?: number
|
||||
address?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export const companyService = {
|
||||
async update(payload: UpdateCompanyPayload): Promise<ApiResponse<Company>> {
|
||||
const { data } = await client.put(API.COMPANY, payload)
|
||||
return data
|
||||
},
|
||||
|
||||
async uploadLogo(payload: FormData): Promise<ApiResponse<Company>> {
|
||||
const { data } = await client.post(API.COMPANY_UPLOAD_LOGO, payload)
|
||||
return data
|
||||
},
|
||||
|
||||
async getSettings(settings?: string[]): Promise<Record<string, string>> {
|
||||
const { data } = await client.get(API.COMPANY_SETTINGS, {
|
||||
params: { settings },
|
||||
})
|
||||
return data
|
||||
},
|
||||
|
||||
async updateSettings(payload: CompanySettingsPayload): Promise<{ success: boolean }> {
|
||||
const { data } = await client.post(API.COMPANY_SETTINGS, payload)
|
||||
return data
|
||||
},
|
||||
|
||||
async hasTransactions(): Promise<{ has_transactions: boolean }> {
|
||||
const { data } = await client.get(API.COMPANY_HAS_TRANSACTIONS)
|
||||
return data
|
||||
},
|
||||
|
||||
async create(payload: CreateCompanyPayload): Promise<ApiResponse<Company>> {
|
||||
const { data } = await client.post(API.COMPANIES, payload)
|
||||
return data
|
||||
},
|
||||
|
||||
async listUserCompanies(): Promise<ApiResponse<Company[]>> {
|
||||
const { data } = await client.get(API.COMPANIES)
|
||||
return data
|
||||
},
|
||||
|
||||
async delete(payload: { id: number }): Promise<{ success: boolean }> {
|
||||
const { data } = await client.post(API.COMPANIES_DELETE, payload)
|
||||
return data
|
||||
},
|
||||
|
||||
async transferOwnership(userId: number): Promise<{ success: boolean }> {
|
||||
const { data } = await client.post(`${API.TRANSFER_OWNERSHIP}/${userId}`)
|
||||
return data
|
||||
},
|
||||
|
||||
// Company Mail Configuration
|
||||
async getMailDefaultConfig(): Promise<Record<string, unknown>> {
|
||||
const { data } = await client.get(API.COMPANY_MAIL_DEFAULT_CONFIG)
|
||||
return data
|
||||
},
|
||||
|
||||
async getMailConfig(): Promise<Record<string, unknown>> {
|
||||
const { data } = await client.get(API.COMPANY_MAIL_CONFIG)
|
||||
return data
|
||||
},
|
||||
|
||||
async saveMailConfig(payload: Record<string, unknown>): Promise<{ success: boolean }> {
|
||||
const { data } = await client.post(API.COMPANY_MAIL_CONFIG, payload)
|
||||
return data
|
||||
},
|
||||
|
||||
async testMailConfig(payload: Record<string, unknown>): Promise<{ success: boolean }> {
|
||||
const { data } = await client.post(API.COMPANY_MAIL_TEST, payload)
|
||||
return data
|
||||
},
|
||||
}
|
||||
47
resources/scripts-v2/api/services/custom-field.service.ts
Normal file
47
resources/scripts-v2/api/services/custom-field.service.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { client } from '../client'
|
||||
import { API } from '../endpoints'
|
||||
import type { CustomField } from '../../types/domain/custom-field'
|
||||
import type { ApiResponse, ListParams } from '../../types/api'
|
||||
|
||||
export interface CustomFieldListParams extends ListParams {
|
||||
model_type?: string
|
||||
type?: string
|
||||
}
|
||||
|
||||
export interface CreateCustomFieldPayload {
|
||||
name: string
|
||||
label: string
|
||||
model_type: string
|
||||
type: string
|
||||
placeholder?: string | null
|
||||
is_required?: boolean
|
||||
options?: Array<{ name: string }> | string[] | null
|
||||
order?: number | null
|
||||
}
|
||||
|
||||
export const customFieldService = {
|
||||
async list(params?: CustomFieldListParams): Promise<ApiResponse<CustomField[]>> {
|
||||
const { data } = await client.get(API.CUSTOM_FIELDS, { params })
|
||||
return data
|
||||
},
|
||||
|
||||
async get(id: number): Promise<ApiResponse<CustomField>> {
|
||||
const { data } = await client.get(`${API.CUSTOM_FIELDS}/${id}`)
|
||||
return data
|
||||
},
|
||||
|
||||
async create(payload: CreateCustomFieldPayload): Promise<ApiResponse<CustomField>> {
|
||||
const { data } = await client.post(API.CUSTOM_FIELDS, payload)
|
||||
return data
|
||||
},
|
||||
|
||||
async update(id: number, payload: Partial<CreateCustomFieldPayload>): Promise<ApiResponse<CustomField>> {
|
||||
const { data } = await client.put(`${API.CUSTOM_FIELDS}/${id}`, payload)
|
||||
return data
|
||||
},
|
||||
|
||||
async delete(id: number): Promise<{ success: boolean; error?: string }> {
|
||||
const { data } = await client.delete(`${API.CUSTOM_FIELDS}/${id}`)
|
||||
return data
|
||||
},
|
||||
}
|
||||
69
resources/scripts-v2/api/services/customer.service.ts
Normal file
69
resources/scripts-v2/api/services/customer.service.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { client } from '../client'
|
||||
import { API } from '../endpoints'
|
||||
import type { Customer, CreateCustomerPayload } from '../../types/domain/customer'
|
||||
import type {
|
||||
ApiResponse,
|
||||
ListParams,
|
||||
DeletePayload,
|
||||
} from '../../types/api'
|
||||
|
||||
export interface CustomerListParams extends ListParams {
|
||||
display_name?: string
|
||||
}
|
||||
|
||||
export interface CustomerListMeta {
|
||||
current_page: number
|
||||
last_page: number
|
||||
per_page: number
|
||||
total: number
|
||||
customer_total_count: number
|
||||
}
|
||||
|
||||
export interface CustomerListResponse {
|
||||
data: Customer[]
|
||||
meta: CustomerListMeta
|
||||
}
|
||||
|
||||
export interface CustomerStatsData {
|
||||
id: number
|
||||
name: string
|
||||
email: string | null
|
||||
total_invoices: number
|
||||
total_estimates: number
|
||||
total_payments: number
|
||||
total_expenses: number
|
||||
total_amount_due: number
|
||||
total_paid: number
|
||||
}
|
||||
|
||||
export const customerService = {
|
||||
async list(params?: CustomerListParams): Promise<CustomerListResponse> {
|
||||
const { data } = await client.get(API.CUSTOMERS, { params })
|
||||
return data
|
||||
},
|
||||
|
||||
async get(id: number): Promise<ApiResponse<Customer>> {
|
||||
const { data } = await client.get(`${API.CUSTOMERS}/${id}`)
|
||||
return data
|
||||
},
|
||||
|
||||
async create(payload: CreateCustomerPayload): Promise<ApiResponse<Customer>> {
|
||||
const { data } = await client.post(API.CUSTOMERS, payload)
|
||||
return data
|
||||
},
|
||||
|
||||
async update(id: number, payload: Partial<CreateCustomerPayload>): Promise<ApiResponse<Customer>> {
|
||||
const { data } = await client.put(`${API.CUSTOMERS}/${id}`, payload)
|
||||
return data
|
||||
},
|
||||
|
||||
async delete(payload: DeletePayload): Promise<{ success: boolean }> {
|
||||
const { data } = await client.post(API.CUSTOMERS_DELETE, payload)
|
||||
return data
|
||||
},
|
||||
|
||||
async getStats(id: number, params?: Record<string, unknown>): Promise<ApiResponse<CustomerStatsData>> {
|
||||
const { data } = await client.get(`${API.CUSTOMER_STATS}/${id}/stats`, { params })
|
||||
return data
|
||||
},
|
||||
}
|
||||
53
resources/scripts-v2/api/services/dashboard.service.ts
Normal file
53
resources/scripts-v2/api/services/dashboard.service.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { client } from '../client'
|
||||
import { API } from '../endpoints'
|
||||
|
||||
export interface DashboardParams {
|
||||
previous_year?: number
|
||||
}
|
||||
|
||||
export interface ChartData {
|
||||
months: string[]
|
||||
invoice_totals: number[]
|
||||
expense_totals: number[]
|
||||
receipt_totals: number[]
|
||||
net_income_totals: number[]
|
||||
}
|
||||
|
||||
export interface DashboardResponse {
|
||||
total_amount_due: number
|
||||
total_customer_count: number
|
||||
total_invoice_count: number
|
||||
total_estimate_count: number
|
||||
chart_data: ChartData
|
||||
total_sales: string
|
||||
total_receipts: string
|
||||
total_expenses: string
|
||||
total_net_income: string
|
||||
recent_due_invoices: Array<{
|
||||
id: number
|
||||
invoice_number: string
|
||||
due_amount: number
|
||||
formatted_due_date: string
|
||||
customer?: {
|
||||
id: number
|
||||
name: string
|
||||
}
|
||||
}>
|
||||
recent_estimates: Array<{
|
||||
id: number
|
||||
estimate_number: string
|
||||
total: number
|
||||
status: string
|
||||
customer?: {
|
||||
id: number
|
||||
name: string
|
||||
}
|
||||
}>
|
||||
}
|
||||
|
||||
export const dashboardService = {
|
||||
async load(params?: DashboardParams): Promise<DashboardResponse> {
|
||||
const { data } = await client.get(API.DASHBOARD, { params })
|
||||
return data
|
||||
},
|
||||
}
|
||||
65
resources/scripts-v2/api/services/disk.service.ts
Normal file
65
resources/scripts-v2/api/services/disk.service.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { client } from '../client'
|
||||
import { API } from '../endpoints'
|
||||
import type { ApiResponse, ListParams } from '../../types/api'
|
||||
|
||||
export interface Disk {
|
||||
id: number
|
||||
name: string
|
||||
driver: string
|
||||
set_as_default: boolean
|
||||
credentials: Record<string, string>
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export interface DiskDriversResponse {
|
||||
drivers: string[]
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
export interface CreateDiskPayload {
|
||||
name: string
|
||||
selected_driver: string
|
||||
// S3/S3-compat/DOSpaces fields
|
||||
key?: string
|
||||
secret?: string
|
||||
region?: string
|
||||
bucket?: string
|
||||
root?: string
|
||||
endpoint?: string
|
||||
// Dropbox fields
|
||||
token?: string
|
||||
app?: string
|
||||
}
|
||||
|
||||
export const diskService = {
|
||||
async list(params?: ListParams): Promise<ApiResponse<Disk[]>> {
|
||||
const { data } = await client.get(API.DISKS, { params })
|
||||
return data
|
||||
},
|
||||
|
||||
async get(disk: string): Promise<Record<string, unknown>> {
|
||||
const { data } = await client.get(`${API.DISKS}/${disk}`)
|
||||
return data
|
||||
},
|
||||
|
||||
async create(payload: CreateDiskPayload): Promise<Disk> {
|
||||
const { data } = await client.post(API.DISKS, payload)
|
||||
return data
|
||||
},
|
||||
|
||||
async update(id: number, payload: Partial<CreateDiskPayload>): Promise<ApiResponse<Disk>> {
|
||||
const { data } = await client.put(`${API.DISKS}/${id}`, payload)
|
||||
return data
|
||||
},
|
||||
|
||||
async delete(id: number): Promise<{ success: boolean }> {
|
||||
const { data } = await client.delete(`${API.DISKS}/${id}`)
|
||||
return data
|
||||
},
|
||||
|
||||
async getDrivers(): Promise<DiskDriversResponse> {
|
||||
const { data } = await client.get(API.DISK_DRIVERS)
|
||||
return data
|
||||
},
|
||||
}
|
||||
114
resources/scripts-v2/api/services/estimate.service.ts
Normal file
114
resources/scripts-v2/api/services/estimate.service.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import { client } from '../client'
|
||||
import { API } from '../endpoints'
|
||||
import type { Estimate, CreateEstimatePayload } from '../../types/domain/estimate'
|
||||
import type { Invoice } from '../../types/domain/invoice'
|
||||
import type {
|
||||
ApiResponse,
|
||||
ListParams,
|
||||
DateRangeParams,
|
||||
NextNumberResponse,
|
||||
DeletePayload,
|
||||
} from '../../types/api'
|
||||
|
||||
export interface EstimateListParams extends ListParams, DateRangeParams {
|
||||
status?: string
|
||||
customer_id?: number
|
||||
}
|
||||
|
||||
export interface EstimateListMeta {
|
||||
current_page: number
|
||||
last_page: number
|
||||
per_page: number
|
||||
total: number
|
||||
estimate_total_count: number
|
||||
}
|
||||
|
||||
export interface EstimateListResponse {
|
||||
data: Estimate[]
|
||||
meta: EstimateListMeta
|
||||
}
|
||||
|
||||
export interface SendEstimatePayload {
|
||||
id: number
|
||||
subject?: string
|
||||
body?: string
|
||||
from?: string
|
||||
to?: string
|
||||
is_preview?: boolean
|
||||
}
|
||||
|
||||
export interface EstimateStatusPayload {
|
||||
id: number
|
||||
status: string
|
||||
}
|
||||
|
||||
export interface EstimateTemplate {
|
||||
name: string
|
||||
path: string
|
||||
}
|
||||
|
||||
export interface EstimateTemplatesResponse {
|
||||
estimateTemplates: EstimateTemplate[]
|
||||
}
|
||||
|
||||
export const estimateService = {
|
||||
async list(params?: EstimateListParams): Promise<EstimateListResponse> {
|
||||
const { data } = await client.get(API.ESTIMATES, { params })
|
||||
return data
|
||||
},
|
||||
|
||||
async get(id: number): Promise<ApiResponse<Estimate>> {
|
||||
const { data } = await client.get(`${API.ESTIMATES}/${id}`)
|
||||
return data
|
||||
},
|
||||
|
||||
async create(payload: CreateEstimatePayload): Promise<ApiResponse<Estimate>> {
|
||||
const { data } = await client.post(API.ESTIMATES, payload)
|
||||
return data
|
||||
},
|
||||
|
||||
async update(id: number, payload: Partial<CreateEstimatePayload>): Promise<ApiResponse<Estimate>> {
|
||||
const { data } = await client.put(`${API.ESTIMATES}/${id}`, payload)
|
||||
return data
|
||||
},
|
||||
|
||||
async delete(payload: DeletePayload): Promise<{ success: boolean }> {
|
||||
const { data } = await client.post(API.ESTIMATES_DELETE, payload)
|
||||
return data
|
||||
},
|
||||
|
||||
async send(payload: SendEstimatePayload): Promise<ApiResponse<Estimate>> {
|
||||
const { data } = await client.post(`${API.ESTIMATES}/${payload.id}/send`, payload)
|
||||
return data
|
||||
},
|
||||
|
||||
async sendPreview(id: number, params?: Record<string, unknown>): Promise<ApiResponse<string>> {
|
||||
const { data } = await client.get(`${API.ESTIMATES}/${id}/send/preview`, { params })
|
||||
return data
|
||||
},
|
||||
|
||||
async clone(id: number): Promise<ApiResponse<Estimate>> {
|
||||
const { data } = await client.post(`${API.ESTIMATES}/${id}/clone`)
|
||||
return data
|
||||
},
|
||||
|
||||
async changeStatus(payload: EstimateStatusPayload): Promise<ApiResponse<Estimate>> {
|
||||
const { data } = await client.post(`${API.ESTIMATES}/${payload.id}/status`, payload)
|
||||
return data
|
||||
},
|
||||
|
||||
async convertToInvoice(id: number): Promise<ApiResponse<Invoice>> {
|
||||
const { data } = await client.post(`${API.ESTIMATES}/${id}/convert-to-invoice`)
|
||||
return data
|
||||
},
|
||||
|
||||
async getNextNumber(params?: { key?: string }): Promise<NextNumberResponse> {
|
||||
const { data } = await client.get(API.NEXT_NUMBER, { params: { key: 'estimate', ...params } })
|
||||
return data
|
||||
},
|
||||
|
||||
async getTemplates(): Promise<EstimateTemplatesResponse> {
|
||||
const { data } = await client.get(API.ESTIMATE_TEMPLATES)
|
||||
return data
|
||||
},
|
||||
}
|
||||
117
resources/scripts-v2/api/services/exchange-rate.service.ts
Normal file
117
resources/scripts-v2/api/services/exchange-rate.service.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import { client } from '../client'
|
||||
import { API } from '../endpoints'
|
||||
import type { ExchangeRateProvider, Currency } from '../../types/domain/currency'
|
||||
import type { ApiResponse, ListParams } from '../../types/api'
|
||||
|
||||
export interface CreateExchangeRateProviderPayload {
|
||||
driver: string
|
||||
key: string
|
||||
active?: boolean
|
||||
currencies?: string[]
|
||||
}
|
||||
|
||||
export interface ExchangeRateResponse {
|
||||
exchange_rate: number
|
||||
}
|
||||
|
||||
export interface ActiveProviderResponse {
|
||||
has_active_provider: boolean
|
||||
exchange_rate: number | null
|
||||
}
|
||||
|
||||
export interface SupportedCurrenciesResponse {
|
||||
supportedCurrencies: string[]
|
||||
}
|
||||
|
||||
export interface UsedCurrenciesResponse {
|
||||
activeUsedCurrencies: Currency[]
|
||||
}
|
||||
|
||||
export interface BulkCurrenciesResponse {
|
||||
currencies: Array<Currency & { exchange_rate: number | null }>
|
||||
}
|
||||
|
||||
export interface BulkUpdatePayload {
|
||||
currencies: Array<{
|
||||
id: number
|
||||
exchange_rate: number
|
||||
}>
|
||||
}
|
||||
|
||||
export interface ConfigDriversResponse {
|
||||
exchange_rate_drivers: string[]
|
||||
}
|
||||
|
||||
export const exchangeRateService = {
|
||||
// Providers CRUD
|
||||
async listProviders(params?: ListParams): Promise<ApiResponse<ExchangeRateProvider[]>> {
|
||||
const { data } = await client.get(API.EXCHANGE_RATE_PROVIDERS, { params })
|
||||
return data
|
||||
},
|
||||
|
||||
async getProvider(id: number): Promise<ApiResponse<ExchangeRateProvider>> {
|
||||
const { data } = await client.get(`${API.EXCHANGE_RATE_PROVIDERS}/${id}`)
|
||||
return data
|
||||
},
|
||||
|
||||
async createProvider(payload: CreateExchangeRateProviderPayload): Promise<ApiResponse<ExchangeRateProvider>> {
|
||||
const { data } = await client.post(API.EXCHANGE_RATE_PROVIDERS, payload)
|
||||
return data
|
||||
},
|
||||
|
||||
async updateProvider(
|
||||
id: number,
|
||||
payload: Partial<CreateExchangeRateProviderPayload>,
|
||||
): Promise<ApiResponse<ExchangeRateProvider>> {
|
||||
const { data } = await client.put(`${API.EXCHANGE_RATE_PROVIDERS}/${id}`, payload)
|
||||
return data
|
||||
},
|
||||
|
||||
async deleteProvider(id: number): Promise<{ success: boolean }> {
|
||||
const { data } = await client.delete(`${API.EXCHANGE_RATE_PROVIDERS}/${id}`)
|
||||
return data
|
||||
},
|
||||
|
||||
// Exchange Rates
|
||||
async getRate(currencyId: number): Promise<ExchangeRateResponse> {
|
||||
const { data } = await client.get(`${API.CURRENCIES}/${currencyId}/exchange-rate`)
|
||||
return data
|
||||
},
|
||||
|
||||
async getActiveProvider(currencyId: number): Promise<ActiveProviderResponse> {
|
||||
const { data } = await client.get(`${API.CURRENCIES}/${currencyId}/active-provider`)
|
||||
return data
|
||||
},
|
||||
|
||||
// Currency lists
|
||||
async getSupportedCurrencies(): Promise<SupportedCurrenciesResponse> {
|
||||
const { data } = await client.get(API.SUPPORTED_CURRENCIES)
|
||||
return data
|
||||
},
|
||||
|
||||
async getUsedCurrencies(): Promise<UsedCurrenciesResponse> {
|
||||
const { data } = await client.get(API.USED_CURRENCIES)
|
||||
return data
|
||||
},
|
||||
|
||||
async getBulkCurrencies(): Promise<BulkCurrenciesResponse> {
|
||||
const { data } = await client.get(API.CURRENCIES_USED)
|
||||
return data
|
||||
},
|
||||
|
||||
async bulkUpdateExchangeRate(payload: BulkUpdatePayload): Promise<{ success: boolean }> {
|
||||
const { data } = await client.post(API.CURRENCIES_BULK_UPDATE, payload)
|
||||
return data
|
||||
},
|
||||
|
||||
// Config
|
||||
async getDrivers(): Promise<ConfigDriversResponse> {
|
||||
const { data } = await client.get(API.CONFIG, { params: { key: 'exchange_rate_drivers' } })
|
||||
return data
|
||||
},
|
||||
|
||||
async getCurrencyConverterServers(): Promise<Record<string, unknown>> {
|
||||
const { data } = await client.get(API.CONFIG, { params: { key: 'currency_converter_servers' } })
|
||||
return data
|
||||
},
|
||||
}
|
||||
100
resources/scripts-v2/api/services/expense.service.ts
Normal file
100
resources/scripts-v2/api/services/expense.service.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import { client } from '../client'
|
||||
import { API } from '../endpoints'
|
||||
import type { Expense, ExpenseCategory, CreateExpensePayload } from '../../types/domain/expense'
|
||||
import type {
|
||||
ApiResponse,
|
||||
ListParams,
|
||||
DateRangeParams,
|
||||
DeletePayload,
|
||||
} from '../../types/api'
|
||||
|
||||
export interface ExpenseListParams extends ListParams, DateRangeParams {
|
||||
expense_category_id?: number
|
||||
customer_id?: number
|
||||
}
|
||||
|
||||
export interface ExpenseListMeta {
|
||||
current_page: number
|
||||
last_page: number
|
||||
per_page: number
|
||||
total: number
|
||||
expense_total_count: number
|
||||
}
|
||||
|
||||
export interface ExpenseListResponse {
|
||||
data: Expense[]
|
||||
meta: ExpenseListMeta
|
||||
}
|
||||
|
||||
export interface CreateExpenseCategoryPayload {
|
||||
name: string
|
||||
description?: string | null
|
||||
}
|
||||
|
||||
export const expenseService = {
|
||||
async list(params?: ExpenseListParams): Promise<ExpenseListResponse> {
|
||||
const { data } = await client.get(API.EXPENSES, { params })
|
||||
return data
|
||||
},
|
||||
|
||||
async get(id: number): Promise<ApiResponse<Expense>> {
|
||||
const { data } = await client.get(`${API.EXPENSES}/${id}`)
|
||||
return data
|
||||
},
|
||||
|
||||
async create(payload: FormData): Promise<ApiResponse<Expense>> {
|
||||
const { data } = await client.post(API.EXPENSES, payload)
|
||||
return data
|
||||
},
|
||||
|
||||
async update(id: number, payload: FormData): Promise<ApiResponse<Expense>> {
|
||||
const { data } = await client.post(`${API.EXPENSES}/${id}`, payload)
|
||||
return data
|
||||
},
|
||||
|
||||
async delete(payload: DeletePayload): Promise<{ success: boolean }> {
|
||||
const { data } = await client.post(API.EXPENSES_DELETE, payload)
|
||||
return data
|
||||
},
|
||||
|
||||
async showReceipt(id: number): Promise<Blob> {
|
||||
const { data } = await client.get(`${API.EXPENSES}/${id}/show/receipt`, {
|
||||
responseType: 'blob',
|
||||
})
|
||||
return data
|
||||
},
|
||||
|
||||
async uploadReceipt(id: number, payload: FormData): Promise<ApiResponse<Expense>> {
|
||||
const { data } = await client.post(`${API.EXPENSES}/${id}/upload/receipts`, payload)
|
||||
return data
|
||||
},
|
||||
|
||||
// Expense Categories
|
||||
async listCategories(params?: ListParams): Promise<ApiResponse<ExpenseCategory[]>> {
|
||||
const { data } = await client.get(API.CATEGORIES, { params })
|
||||
return data
|
||||
},
|
||||
|
||||
async getCategory(id: number): Promise<ApiResponse<ExpenseCategory>> {
|
||||
const { data } = await client.get(`${API.CATEGORIES}/${id}`)
|
||||
return data
|
||||
},
|
||||
|
||||
async createCategory(payload: CreateExpenseCategoryPayload): Promise<ApiResponse<ExpenseCategory>> {
|
||||
const { data } = await client.post(API.CATEGORIES, payload)
|
||||
return data
|
||||
},
|
||||
|
||||
async updateCategory(
|
||||
id: number,
|
||||
payload: CreateExpenseCategoryPayload,
|
||||
): Promise<ApiResponse<ExpenseCategory>> {
|
||||
const { data } = await client.put(`${API.CATEGORIES}/${id}`, payload)
|
||||
return data
|
||||
},
|
||||
|
||||
async deleteCategory(id: number): Promise<{ success: boolean }> {
|
||||
const { data } = await client.delete(`${API.CATEGORIES}/${id}`)
|
||||
return data
|
||||
},
|
||||
}
|
||||
52
resources/scripts-v2/api/services/index.ts
Normal file
52
resources/scripts-v2/api/services/index.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
export { authService } from './auth.service'
|
||||
export { bootstrapService } from './bootstrap.service'
|
||||
export { invoiceService } from './invoice.service'
|
||||
export { estimateService } from './estimate.service'
|
||||
export { recurringInvoiceService } from './recurring-invoice.service'
|
||||
export { customerService } from './customer.service'
|
||||
export { paymentService } from './payment.service'
|
||||
export { expenseService } from './expense.service'
|
||||
export { itemService } from './item.service'
|
||||
export { companyService } from './company.service'
|
||||
export { userService } from './user.service'
|
||||
export { memberService } from './member.service'
|
||||
export { settingService } from './setting.service'
|
||||
export { dashboardService } from './dashboard.service'
|
||||
export { reportService } from './report.service'
|
||||
export { roleService } from './role.service'
|
||||
export { taxTypeService } from './tax-type.service'
|
||||
export { customFieldService } from './custom-field.service'
|
||||
export { noteService } from './note.service'
|
||||
export { exchangeRateService } from './exchange-rate.service'
|
||||
export { moduleService } from './module.service'
|
||||
export { backupService } from './backup.service'
|
||||
export { mailService } from './mail.service'
|
||||
export { pdfService } from './pdf.service'
|
||||
export { diskService } from './disk.service'
|
||||
|
||||
// Re-export service types for convenience
|
||||
export type { LoginPayload, LoginResponse, ForgotPasswordPayload, ResetPasswordPayload, RegisterWithInvitationPayload } from './auth.service'
|
||||
export type { BootstrapResponse, MenuItem, CurrentCompanyResponse } from './bootstrap.service'
|
||||
export type { InvoiceListParams, InvoiceListResponse, SendInvoicePayload, InvoiceStatusPayload, InvoiceTemplatesResponse } from './invoice.service'
|
||||
export type { EstimateListParams, EstimateListResponse, SendEstimatePayload, EstimateStatusPayload, EstimateTemplatesResponse } from './estimate.service'
|
||||
export type { RecurringInvoiceListParams, RecurringInvoiceListResponse, FrequencyDateParams, FrequencyDateResponse } from './recurring-invoice.service'
|
||||
export type { CustomerListParams, CustomerListResponse, CustomerStatsData } from './customer.service'
|
||||
export type { PaymentListParams, PaymentListResponse, SendPaymentPayload, CreatePaymentMethodPayload } from './payment.service'
|
||||
export type { ExpenseListParams, ExpenseListResponse, CreateExpenseCategoryPayload } from './expense.service'
|
||||
export type { ItemListParams, ItemListResponse, CreateItemPayload, CreateUnitPayload } from './item.service'
|
||||
export type { UpdateCompanyPayload, CompanySettingsPayload, CreateCompanyPayload } from './company.service'
|
||||
export type { UpdateProfilePayload, UserSettingsPayload } from './user.service'
|
||||
export type { MemberListParams, MemberListResponse, UpdateMemberPayload, InviteMemberPayload, DeleteMembersPayload } from './member.service'
|
||||
export type { ConfigResponse, GlobalSettingsPayload, DateFormat, TimeFormat } from './setting.service'
|
||||
export type { DashboardParams, DashboardResponse, ChartData } from './dashboard.service'
|
||||
export type { ReportParams, SalesReportResponse, ProfitLossReportResponse, ExpenseReportResponse, TaxReportResponse } from './report.service'
|
||||
export type { CreateRolePayload, AbilitiesResponse } from './role.service'
|
||||
export type { CreateTaxTypePayload } from './tax-type.service'
|
||||
export type { CustomFieldListParams, CreateCustomFieldPayload } from './custom-field.service'
|
||||
export type { CreateNotePayload } from './note.service'
|
||||
export type { CreateExchangeRateProviderPayload, BulkUpdatePayload, ExchangeRateResponse, ActiveProviderResponse } from './exchange-rate.service'
|
||||
export type { Module, ModuleInstallPayload, ModuleCheckResponse } from './module.service'
|
||||
export type { Backup, CreateBackupPayload, DeleteBackupParams } from './backup.service'
|
||||
export type { MailConfig, MailConfigResponse, MailDriver, SmtpConfig, MailgunConfig, SesConfig, TestMailPayload } from './mail.service'
|
||||
export type { PdfConfig, PdfConfigResponse, PdfDriver, DomPdfConfig, GotenbergConfig } from './pdf.service'
|
||||
export type { Disk, DiskDriversResponse, CreateDiskPayload } from './disk.service'
|
||||
112
resources/scripts-v2/api/services/invoice.service.ts
Normal file
112
resources/scripts-v2/api/services/invoice.service.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import { client } from '../client'
|
||||
import { API } from '../endpoints'
|
||||
import type { Invoice, CreateInvoicePayload } from '../../types/domain/invoice'
|
||||
import type {
|
||||
ApiResponse,
|
||||
PaginatedResponse,
|
||||
ListParams,
|
||||
DateRangeParams,
|
||||
NextNumberResponse,
|
||||
DeletePayload,
|
||||
} from '../../types/api'
|
||||
|
||||
export interface InvoiceListParams extends ListParams, DateRangeParams {
|
||||
status?: string
|
||||
customer_id?: number
|
||||
}
|
||||
|
||||
export interface InvoiceListMeta {
|
||||
current_page: number
|
||||
last_page: number
|
||||
per_page: number
|
||||
total: number
|
||||
invoice_total_count: number
|
||||
}
|
||||
|
||||
export interface InvoiceListResponse {
|
||||
data: Invoice[]
|
||||
meta: InvoiceListMeta
|
||||
}
|
||||
|
||||
export interface SendInvoicePayload {
|
||||
id: number
|
||||
subject?: string
|
||||
body?: string
|
||||
from?: string
|
||||
to?: string
|
||||
}
|
||||
|
||||
export interface InvoiceStatusPayload {
|
||||
id: number
|
||||
status: string
|
||||
}
|
||||
|
||||
export interface SendPreviewParams {
|
||||
id: number
|
||||
}
|
||||
|
||||
export interface InvoiceTemplate {
|
||||
name: string
|
||||
path: string
|
||||
}
|
||||
|
||||
export interface InvoiceTemplatesResponse {
|
||||
invoiceTemplates: InvoiceTemplate[]
|
||||
}
|
||||
|
||||
export const invoiceService = {
|
||||
async list(params?: InvoiceListParams): Promise<InvoiceListResponse> {
|
||||
const { data } = await client.get(API.INVOICES, { params })
|
||||
return data
|
||||
},
|
||||
|
||||
async get(id: number): Promise<ApiResponse<Invoice>> {
|
||||
const { data } = await client.get(`${API.INVOICES}/${id}`)
|
||||
return data
|
||||
},
|
||||
|
||||
async create(payload: CreateInvoicePayload): Promise<ApiResponse<Invoice>> {
|
||||
const { data } = await client.post(API.INVOICES, payload)
|
||||
return data
|
||||
},
|
||||
|
||||
async update(id: number, payload: Partial<CreateInvoicePayload>): Promise<ApiResponse<Invoice>> {
|
||||
const { data } = await client.put(`${API.INVOICES}/${id}`, payload)
|
||||
return data
|
||||
},
|
||||
|
||||
async delete(payload: DeletePayload): Promise<{ success: boolean }> {
|
||||
const { data } = await client.post(API.INVOICES_DELETE, payload)
|
||||
return data
|
||||
},
|
||||
|
||||
async send(payload: SendInvoicePayload): Promise<ApiResponse<Invoice>> {
|
||||
const { data } = await client.post(`${API.INVOICES}/${payload.id}/send`, payload)
|
||||
return data
|
||||
},
|
||||
|
||||
async sendPreview(params: SendPreviewParams): Promise<ApiResponse<string>> {
|
||||
const { data } = await client.get(`${API.INVOICES}/${params.id}/send/preview`, { params })
|
||||
return data
|
||||
},
|
||||
|
||||
async clone(id: number): Promise<ApiResponse<Invoice>> {
|
||||
const { data } = await client.post(`${API.INVOICES}/${id}/clone`)
|
||||
return data
|
||||
},
|
||||
|
||||
async changeStatus(payload: InvoiceStatusPayload): Promise<ApiResponse<Invoice>> {
|
||||
const { data } = await client.post(`${API.INVOICES}/${payload.id}/status`, payload)
|
||||
return data
|
||||
},
|
||||
|
||||
async getNextNumber(params?: { key?: string }): Promise<NextNumberResponse> {
|
||||
const { data } = await client.get(API.NEXT_NUMBER, { params: { key: 'invoice', ...params } })
|
||||
return data
|
||||
},
|
||||
|
||||
async getTemplates(): Promise<InvoiceTemplatesResponse> {
|
||||
const { data } = await client.get(API.INVOICE_TEMPLATES)
|
||||
return data
|
||||
},
|
||||
}
|
||||
90
resources/scripts-v2/api/services/item.service.ts
Normal file
90
resources/scripts-v2/api/services/item.service.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { client } from '../client'
|
||||
import { API } from '../endpoints'
|
||||
import type { Item, Unit } from '../../types/domain/item'
|
||||
import type {
|
||||
ApiResponse,
|
||||
ListParams,
|
||||
DeletePayload,
|
||||
} from '../../types/api'
|
||||
|
||||
export interface ItemListParams extends ListParams {
|
||||
filter?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export interface ItemListMeta {
|
||||
current_page: number
|
||||
last_page: number
|
||||
per_page: number
|
||||
total: number
|
||||
item_total_count: number
|
||||
}
|
||||
|
||||
export interface ItemListResponse {
|
||||
data: Item[]
|
||||
meta: ItemListMeta
|
||||
}
|
||||
|
||||
export interface CreateItemPayload {
|
||||
name: string
|
||||
description?: string | null
|
||||
price: number
|
||||
unit_id?: number | null
|
||||
taxes?: Array<{ tax_type_id: number }>
|
||||
}
|
||||
|
||||
export interface CreateUnitPayload {
|
||||
name: string
|
||||
}
|
||||
|
||||
export const itemService = {
|
||||
async list(params?: ItemListParams): Promise<ItemListResponse> {
|
||||
const { data } = await client.get(API.ITEMS, { params })
|
||||
return data
|
||||
},
|
||||
|
||||
async get(id: number): Promise<ApiResponse<Item>> {
|
||||
const { data } = await client.get(`${API.ITEMS}/${id}`)
|
||||
return data
|
||||
},
|
||||
|
||||
async create(payload: CreateItemPayload): Promise<ApiResponse<Item>> {
|
||||
const { data } = await client.post(API.ITEMS, payload)
|
||||
return data
|
||||
},
|
||||
|
||||
async update(id: number, payload: Partial<CreateItemPayload>): Promise<ApiResponse<Item>> {
|
||||
const { data } = await client.put(`${API.ITEMS}/${id}`, payload)
|
||||
return data
|
||||
},
|
||||
|
||||
async delete(payload: DeletePayload): Promise<{ success: boolean }> {
|
||||
const { data } = await client.post(API.ITEMS_DELETE, payload)
|
||||
return data
|
||||
},
|
||||
|
||||
// Units
|
||||
async listUnits(params?: ListParams): Promise<ApiResponse<Unit[]>> {
|
||||
const { data } = await client.get(API.UNITS, { params })
|
||||
return data
|
||||
},
|
||||
|
||||
async getUnit(id: number): Promise<ApiResponse<Unit>> {
|
||||
const { data } = await client.get(`${API.UNITS}/${id}`)
|
||||
return data
|
||||
},
|
||||
|
||||
async createUnit(payload: CreateUnitPayload): Promise<ApiResponse<Unit>> {
|
||||
const { data } = await client.post(API.UNITS, payload)
|
||||
return data
|
||||
},
|
||||
|
||||
async updateUnit(id: number, payload: CreateUnitPayload): Promise<ApiResponse<Unit>> {
|
||||
const { data } = await client.put(`${API.UNITS}/${id}`, payload)
|
||||
return data
|
||||
},
|
||||
|
||||
async deleteUnit(id: number): Promise<{ success: boolean }> {
|
||||
const { data } = await client.delete(`${API.UNITS}/${id}`)
|
||||
return data
|
||||
},
|
||||
}
|
||||
71
resources/scripts-v2/api/services/mail.service.ts
Normal file
71
resources/scripts-v2/api/services/mail.service.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { client } from '../client'
|
||||
import { API } from '../endpoints'
|
||||
|
||||
export interface MailDriver {
|
||||
name: string
|
||||
value: string
|
||||
}
|
||||
|
||||
export interface SmtpConfig {
|
||||
mail_driver: string
|
||||
mail_host: string
|
||||
mail_port: number | null
|
||||
mail_username: string
|
||||
mail_password: string
|
||||
mail_encryption: string
|
||||
from_mail: string
|
||||
from_name: string
|
||||
}
|
||||
|
||||
export interface MailgunConfig {
|
||||
mail_driver: string
|
||||
mail_mailgun_domain: string
|
||||
mail_mailgun_secret: string
|
||||
mail_mailgun_endpoint: string
|
||||
from_mail: string
|
||||
from_name: string
|
||||
}
|
||||
|
||||
export interface SesConfig {
|
||||
mail_driver: string
|
||||
mail_host: string
|
||||
mail_port: number | null
|
||||
mail_ses_key: string
|
||||
mail_ses_secret: string
|
||||
mail_ses_region: string
|
||||
from_mail: string
|
||||
from_name: string
|
||||
}
|
||||
|
||||
export type MailConfig = SmtpConfig | MailgunConfig | SesConfig
|
||||
|
||||
export interface MailConfigResponse {
|
||||
mail_driver: string
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
export interface TestMailPayload {
|
||||
to: string
|
||||
}
|
||||
|
||||
export const mailService = {
|
||||
async getDrivers(): Promise<MailDriver[]> {
|
||||
const { data } = await client.get(API.MAIL_DRIVERS)
|
||||
return data
|
||||
},
|
||||
|
||||
async getConfig(): Promise<MailConfigResponse> {
|
||||
const { data } = await client.get(API.MAIL_CONFIG)
|
||||
return data
|
||||
},
|
||||
|
||||
async saveConfig(payload: MailConfig): Promise<{ success?: string; error?: string }> {
|
||||
const { data } = await client.post(API.MAIL_CONFIG, payload)
|
||||
return data
|
||||
},
|
||||
|
||||
async testEmail(payload: TestMailPayload): Promise<{ success?: boolean; error?: string }> {
|
||||
const { data } = await client.post(API.MAIL_TEST, payload)
|
||||
return data
|
||||
},
|
||||
}
|
||||
102
resources/scripts-v2/api/services/member.service.ts
Normal file
102
resources/scripts-v2/api/services/member.service.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { client } from '../client'
|
||||
import { API } from '../endpoints'
|
||||
import type { User } from '../../types/domain/user'
|
||||
import type { CompanyInvitation } from '../../types/domain/company'
|
||||
import type { ApiResponse, PaginatedResponse, ListParams } from '../../types/api'
|
||||
|
||||
export interface MemberListParams extends ListParams {
|
||||
display_name?: string
|
||||
}
|
||||
|
||||
export interface MemberListResponse {
|
||||
data: User[]
|
||||
meta: {
|
||||
current_page: number
|
||||
last_page: number
|
||||
per_page: number
|
||||
total: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface UpdateMemberPayload {
|
||||
name?: string
|
||||
email?: string
|
||||
phone?: string | null
|
||||
role?: string | null
|
||||
companies?: Array<{
|
||||
id: number
|
||||
role?: string
|
||||
}>
|
||||
}
|
||||
|
||||
export interface InviteMemberPayload {
|
||||
email: string
|
||||
role?: string
|
||||
}
|
||||
|
||||
export interface DeleteMembersPayload {
|
||||
users: number[]
|
||||
}
|
||||
|
||||
export interface PendingInvitationsResponse {
|
||||
invitations: CompanyInvitation[]
|
||||
}
|
||||
|
||||
export const memberService = {
|
||||
async list(params?: MemberListParams): Promise<MemberListResponse> {
|
||||
const { data } = await client.get(API.MEMBERS, { params })
|
||||
return data
|
||||
},
|
||||
|
||||
async get(id: number): Promise<ApiResponse<User>> {
|
||||
const { data } = await client.get(`${API.MEMBERS}/${id}`)
|
||||
return data
|
||||
},
|
||||
|
||||
async create(payload: UpdateMemberPayload): Promise<ApiResponse<User>> {
|
||||
const { data } = await client.post(API.MEMBERS, payload)
|
||||
return data
|
||||
},
|
||||
|
||||
async update(id: number, payload: UpdateMemberPayload): Promise<ApiResponse<User>> {
|
||||
const { data } = await client.put(`${API.MEMBERS}/${id}`, payload)
|
||||
return data
|
||||
},
|
||||
|
||||
async delete(payload: DeleteMembersPayload): Promise<{ success: boolean }> {
|
||||
const { data } = await client.post(API.MEMBERS_DELETE, payload)
|
||||
return data
|
||||
},
|
||||
|
||||
// Company Invitations (send invitations)
|
||||
async fetchPendingInvitations(): Promise<PendingInvitationsResponse> {
|
||||
const { data } = await client.get(API.COMPANY_INVITATIONS)
|
||||
return data
|
||||
},
|
||||
|
||||
async invite(payload: InviteMemberPayload): Promise<ApiResponse<CompanyInvitation>> {
|
||||
const { data } = await client.post(API.COMPANY_INVITATIONS, payload)
|
||||
return data
|
||||
},
|
||||
|
||||
async cancelInvitation(id: number): Promise<{ success: boolean }> {
|
||||
const { data } = await client.delete(`${API.COMPANY_INVITATIONS}/${id}`)
|
||||
return data
|
||||
},
|
||||
|
||||
// User-scoped invitation responses
|
||||
async fetchUserPendingInvitations(): Promise<PendingInvitationsResponse> {
|
||||
const { data } = await client.get(API.INVITATIONS_PENDING)
|
||||
return data
|
||||
},
|
||||
|
||||
async acceptInvitation(token: string): Promise<{ success: boolean }> {
|
||||
const { data } = await client.post(`${API.INVITATIONS}/${token}/accept`)
|
||||
return data
|
||||
},
|
||||
|
||||
async declineInvitation(token: string): Promise<{ success: boolean }> {
|
||||
const { data } = await client.post(`${API.INVITATIONS}/${token}/decline`)
|
||||
return data
|
||||
},
|
||||
}
|
||||
77
resources/scripts-v2/api/services/module.service.ts
Normal file
77
resources/scripts-v2/api/services/module.service.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { client } from '../client'
|
||||
import { API } from '../endpoints'
|
||||
import type { ApiResponse } from '../../types/api'
|
||||
|
||||
export interface Module {
|
||||
name: string
|
||||
slug: string
|
||||
description: string
|
||||
version: string
|
||||
enabled: boolean
|
||||
installed: boolean
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
export interface ModuleCheckResponse {
|
||||
error?: string
|
||||
success?: boolean
|
||||
}
|
||||
|
||||
export interface ModuleInstallPayload {
|
||||
module: string
|
||||
version: string
|
||||
api_token?: string
|
||||
}
|
||||
|
||||
export const moduleService = {
|
||||
async list(): Promise<ApiResponse<Module[]>> {
|
||||
const { data } = await client.get(API.MODULES)
|
||||
return data
|
||||
},
|
||||
|
||||
async get(module: string): Promise<Module> {
|
||||
const { data } = await client.get(`${API.MODULES}/${module}`)
|
||||
return data
|
||||
},
|
||||
|
||||
async checkToken(apiToken: string): Promise<ModuleCheckResponse> {
|
||||
const { data } = await client.get(`${API.MODULES_CHECK}?api_token=${apiToken}`)
|
||||
return data
|
||||
},
|
||||
|
||||
async enable(module: string): Promise<{ success: boolean }> {
|
||||
const { data } = await client.post(`${API.MODULES}/${module}/enable`)
|
||||
return data
|
||||
},
|
||||
|
||||
async disable(module: string): Promise<{ success: boolean }> {
|
||||
const { data } = await client.post(`${API.MODULES}/${module}/disable`)
|
||||
return data
|
||||
},
|
||||
|
||||
// Installation flow
|
||||
async download(payload: ModuleInstallPayload): Promise<{ success: boolean }> {
|
||||
const { data } = await client.post(API.MODULES_DOWNLOAD, payload)
|
||||
return data
|
||||
},
|
||||
|
||||
async upload(payload: FormData): Promise<{ success: boolean }> {
|
||||
const { data } = await client.post(API.MODULES_UPLOAD, payload)
|
||||
return data
|
||||
},
|
||||
|
||||
async unzip(payload: ModuleInstallPayload): Promise<{ success: boolean }> {
|
||||
const { data } = await client.post(API.MODULES_UNZIP, payload)
|
||||
return data
|
||||
},
|
||||
|
||||
async copy(payload: ModuleInstallPayload): Promise<{ success: boolean }> {
|
||||
const { data } = await client.post(API.MODULES_COPY, payload)
|
||||
return data
|
||||
},
|
||||
|
||||
async complete(payload: ModuleInstallPayload): Promise<{ success: boolean }> {
|
||||
const { data } = await client.post(API.MODULES_COMPLETE, payload)
|
||||
return data
|
||||
},
|
||||
}
|
||||
38
resources/scripts-v2/api/services/note.service.ts
Normal file
38
resources/scripts-v2/api/services/note.service.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { client } from '../client'
|
||||
import { API } from '../endpoints'
|
||||
import type { Note } from '../../types/domain/note'
|
||||
import type { ApiResponse, ListParams } from '../../types/api'
|
||||
|
||||
export interface CreateNotePayload {
|
||||
type: string
|
||||
name: string
|
||||
notes: string
|
||||
is_default?: boolean
|
||||
}
|
||||
|
||||
export const noteService = {
|
||||
async list(params?: ListParams): Promise<ApiResponse<Note[]>> {
|
||||
const { data } = await client.get(API.NOTES, { params })
|
||||
return data
|
||||
},
|
||||
|
||||
async get(id: number): Promise<ApiResponse<Note>> {
|
||||
const { data } = await client.get(`${API.NOTES}/${id}`)
|
||||
return data
|
||||
},
|
||||
|
||||
async create(payload: CreateNotePayload): Promise<Note> {
|
||||
const { data } = await client.post(API.NOTES, payload)
|
||||
return data
|
||||
},
|
||||
|
||||
async update(id: number, payload: Partial<CreateNotePayload>): Promise<ApiResponse<Note>> {
|
||||
const { data } = await client.put(`${API.NOTES}/${id}`, payload)
|
||||
return data
|
||||
},
|
||||
|
||||
async delete(id: number): Promise<{ success: boolean }> {
|
||||
const { data } = await client.delete(`${API.NOTES}/${id}`)
|
||||
return data
|
||||
},
|
||||
}
|
||||
108
resources/scripts-v2/api/services/payment.service.ts
Normal file
108
resources/scripts-v2/api/services/payment.service.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import { client } from '../client'
|
||||
import { API } from '../endpoints'
|
||||
import type { Payment, PaymentMethod, CreatePaymentPayload } from '../../types/domain/payment'
|
||||
import type {
|
||||
ApiResponse,
|
||||
ListParams,
|
||||
NextNumberResponse,
|
||||
DeletePayload,
|
||||
} from '../../types/api'
|
||||
|
||||
export interface PaymentListParams extends ListParams {
|
||||
customer_id?: number
|
||||
from_date?: string
|
||||
to_date?: string
|
||||
}
|
||||
|
||||
export interface PaymentListMeta {
|
||||
current_page: number
|
||||
last_page: number
|
||||
per_page: number
|
||||
total: number
|
||||
payment_total_count: number
|
||||
}
|
||||
|
||||
export interface PaymentListResponse {
|
||||
data: Payment[]
|
||||
meta: PaymentListMeta
|
||||
}
|
||||
|
||||
export interface SendPaymentPayload {
|
||||
id: number
|
||||
subject?: string
|
||||
body?: string
|
||||
from?: string
|
||||
to?: string
|
||||
}
|
||||
|
||||
export interface CreatePaymentMethodPayload {
|
||||
name: string
|
||||
}
|
||||
|
||||
export const paymentService = {
|
||||
async list(params?: PaymentListParams): Promise<PaymentListResponse> {
|
||||
const { data } = await client.get(API.PAYMENTS, { params })
|
||||
return data
|
||||
},
|
||||
|
||||
async get(id: number): Promise<ApiResponse<Payment>> {
|
||||
const { data } = await client.get(`${API.PAYMENTS}/${id}`)
|
||||
return data
|
||||
},
|
||||
|
||||
async create(payload: CreatePaymentPayload): Promise<ApiResponse<Payment>> {
|
||||
const { data } = await client.post(API.PAYMENTS, payload)
|
||||
return data
|
||||
},
|
||||
|
||||
async update(id: number, payload: Partial<CreatePaymentPayload>): Promise<ApiResponse<Payment>> {
|
||||
const { data } = await client.put(`${API.PAYMENTS}/${id}`, payload)
|
||||
return data
|
||||
},
|
||||
|
||||
async delete(payload: DeletePayload): Promise<{ success: boolean }> {
|
||||
const { data } = await client.post(API.PAYMENTS_DELETE, payload)
|
||||
return data
|
||||
},
|
||||
|
||||
async send(payload: SendPaymentPayload): Promise<ApiResponse<Payment>> {
|
||||
const { data } = await client.post(`${API.PAYMENTS}/${payload.id}/send`, payload)
|
||||
return data
|
||||
},
|
||||
|
||||
async sendPreview(id: number, params?: Record<string, unknown>): Promise<ApiResponse<string>> {
|
||||
const { data } = await client.get(`${API.PAYMENTS}/${id}/send/preview`, { params })
|
||||
return data
|
||||
},
|
||||
|
||||
async getNextNumber(params?: { key?: string }): Promise<NextNumberResponse> {
|
||||
const { data } = await client.get(API.NEXT_NUMBER, { params: { key: 'payment', ...params } })
|
||||
return data
|
||||
},
|
||||
|
||||
// Payment Methods
|
||||
async listMethods(params?: ListParams): Promise<ApiResponse<PaymentMethod[]>> {
|
||||
const { data } = await client.get(API.PAYMENT_METHODS, { params })
|
||||
return data
|
||||
},
|
||||
|
||||
async getMethod(id: number): Promise<ApiResponse<PaymentMethod>> {
|
||||
const { data } = await client.get(`${API.PAYMENT_METHODS}/${id}`)
|
||||
return data
|
||||
},
|
||||
|
||||
async createMethod(payload: CreatePaymentMethodPayload): Promise<ApiResponse<PaymentMethod>> {
|
||||
const { data } = await client.post(API.PAYMENT_METHODS, payload)
|
||||
return data
|
||||
},
|
||||
|
||||
async updateMethod(id: number, payload: CreatePaymentMethodPayload): Promise<ApiResponse<PaymentMethod>> {
|
||||
const { data } = await client.put(`${API.PAYMENT_METHODS}/${id}`, payload)
|
||||
return data
|
||||
},
|
||||
|
||||
async deleteMethod(id: number): Promise<{ success: boolean }> {
|
||||
const { data } = await client.delete(`${API.PAYMENT_METHODS}/${id}`)
|
||||
return data
|
||||
},
|
||||
}
|
||||
41
resources/scripts-v2/api/services/pdf.service.ts
Normal file
41
resources/scripts-v2/api/services/pdf.service.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { client } from '../client'
|
||||
import { API } from '../endpoints'
|
||||
|
||||
export interface PdfDriver {
|
||||
name: string
|
||||
value: string
|
||||
}
|
||||
|
||||
export interface DomPdfConfig {
|
||||
pdf_driver: string
|
||||
}
|
||||
|
||||
export interface GotenbergConfig {
|
||||
pdf_driver: string
|
||||
gotenberg_host: string
|
||||
gotenberg_papersize: string
|
||||
}
|
||||
|
||||
export type PdfConfig = DomPdfConfig | GotenbergConfig
|
||||
|
||||
export interface PdfConfigResponse {
|
||||
pdf_driver: string
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
export const pdfService = {
|
||||
async getDrivers(): Promise<PdfDriver[]> {
|
||||
const { data } = await client.get(API.PDF_DRIVERS)
|
||||
return data
|
||||
},
|
||||
|
||||
async getConfig(): Promise<PdfConfigResponse> {
|
||||
const { data } = await client.get(API.PDF_CONFIG)
|
||||
return data
|
||||
},
|
||||
|
||||
async saveConfig(payload: PdfConfig): Promise<{ success?: string; error?: string }> {
|
||||
const { data } = await client.post(API.PDF_CONFIG, payload)
|
||||
return data
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import { client } from '../client'
|
||||
import { API } from '../endpoints'
|
||||
import type { RecurringInvoice, CreateRecurringInvoicePayload } from '../../types/domain/recurring-invoice'
|
||||
import type {
|
||||
ApiResponse,
|
||||
ListParams,
|
||||
DeletePayload,
|
||||
} from '../../types/api'
|
||||
|
||||
export interface RecurringInvoiceListParams extends ListParams {
|
||||
status?: string
|
||||
customer_id?: number
|
||||
}
|
||||
|
||||
export interface RecurringInvoiceListMeta {
|
||||
current_page: number
|
||||
last_page: number
|
||||
per_page: number
|
||||
total: number
|
||||
recurring_invoice_total_count: number
|
||||
}
|
||||
|
||||
export interface RecurringInvoiceListResponse {
|
||||
data: RecurringInvoice[]
|
||||
meta: RecurringInvoiceListMeta
|
||||
}
|
||||
|
||||
export interface FrequencyDateParams {
|
||||
frequency: string
|
||||
starts_at?: string
|
||||
}
|
||||
|
||||
export interface FrequencyDateResponse {
|
||||
next_invoice_at: string
|
||||
}
|
||||
|
||||
export const recurringInvoiceService = {
|
||||
async list(params?: RecurringInvoiceListParams): Promise<RecurringInvoiceListResponse> {
|
||||
const { data } = await client.get(API.RECURRING_INVOICES, { params })
|
||||
return data
|
||||
},
|
||||
|
||||
async get(id: number): Promise<ApiResponse<RecurringInvoice>> {
|
||||
const { data } = await client.get(`${API.RECURRING_INVOICES}/${id}`)
|
||||
return data
|
||||
},
|
||||
|
||||
async create(payload: CreateRecurringInvoicePayload): Promise<ApiResponse<RecurringInvoice>> {
|
||||
const { data } = await client.post(API.RECURRING_INVOICES, payload)
|
||||
return data
|
||||
},
|
||||
|
||||
async update(
|
||||
id: number,
|
||||
payload: Partial<CreateRecurringInvoicePayload>,
|
||||
): Promise<ApiResponse<RecurringInvoice>> {
|
||||
const { data } = await client.put(`${API.RECURRING_INVOICES}/${id}`, payload)
|
||||
return data
|
||||
},
|
||||
|
||||
async delete(payload: DeletePayload): Promise<{ success: boolean }> {
|
||||
const { data } = await client.post(API.RECURRING_INVOICES_DELETE, payload)
|
||||
return data
|
||||
},
|
||||
|
||||
async getFrequencyDate(params: FrequencyDateParams): Promise<FrequencyDateResponse> {
|
||||
const { data } = await client.get(API.RECURRING_INVOICE_FREQUENCY, { params })
|
||||
return data
|
||||
},
|
||||
}
|
||||
86
resources/scripts-v2/api/services/report.service.ts
Normal file
86
resources/scripts-v2/api/services/report.service.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { client } from '../client'
|
||||
import { API } from '../endpoints'
|
||||
import type { DateRangeParams } from '../../types/api'
|
||||
|
||||
export interface ReportParams extends DateRangeParams {
|
||||
report_type?: string
|
||||
}
|
||||
|
||||
export interface SalesReportResponse {
|
||||
data: Array<{
|
||||
date: string
|
||||
total: number
|
||||
count: number
|
||||
}>
|
||||
total: number
|
||||
from_date: string
|
||||
to_date: string
|
||||
}
|
||||
|
||||
export interface ProfitLossReportResponse {
|
||||
data: {
|
||||
income: Array<{
|
||||
label: string
|
||||
amount: number
|
||||
}>
|
||||
expenses: Array<{
|
||||
label: string
|
||||
amount: number
|
||||
}>
|
||||
net_profit: number
|
||||
}
|
||||
from_date: string
|
||||
to_date: string
|
||||
}
|
||||
|
||||
export interface ExpenseReportResponse {
|
||||
data: Array<{
|
||||
category: string
|
||||
total: number
|
||||
count: number
|
||||
}>
|
||||
total: number
|
||||
from_date: string
|
||||
to_date: string
|
||||
}
|
||||
|
||||
export interface TaxReportResponse {
|
||||
data: Array<{
|
||||
tax_name: string
|
||||
tax_amount: number
|
||||
invoice_count: number
|
||||
}>
|
||||
total: number
|
||||
from_date: string
|
||||
to_date: string
|
||||
}
|
||||
|
||||
export const reportService = {
|
||||
async getSalesReport(params: ReportParams): Promise<SalesReportResponse> {
|
||||
const { data } = await client.get(API.DASHBOARD, {
|
||||
params: { ...params, report_type: 'sales' },
|
||||
})
|
||||
return data
|
||||
},
|
||||
|
||||
async getProfitLossReport(params: ReportParams): Promise<ProfitLossReportResponse> {
|
||||
const { data } = await client.get(API.DASHBOARD, {
|
||||
params: { ...params, report_type: 'profit_loss' },
|
||||
})
|
||||
return data
|
||||
},
|
||||
|
||||
async getExpenseReport(params: ReportParams): Promise<ExpenseReportResponse> {
|
||||
const { data } = await client.get(API.DASHBOARD, {
|
||||
params: { ...params, report_type: 'expenses' },
|
||||
})
|
||||
return data
|
||||
},
|
||||
|
||||
async getTaxReport(params: ReportParams): Promise<TaxReportResponse> {
|
||||
const { data } = await client.get(API.DASHBOARD, {
|
||||
params: { ...params, report_type: 'tax' },
|
||||
})
|
||||
return data
|
||||
},
|
||||
}
|
||||
48
resources/scripts-v2/api/services/role.service.ts
Normal file
48
resources/scripts-v2/api/services/role.service.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { client } from '../client'
|
||||
import { API } from '../endpoints'
|
||||
import type { Role, Ability } from '../../types/domain/role'
|
||||
import type { ApiResponse, ListParams } from '../../types/api'
|
||||
|
||||
export interface CreateRolePayload {
|
||||
name: string
|
||||
abilities: Array<{
|
||||
ability: string
|
||||
model?: string | null
|
||||
}>
|
||||
}
|
||||
|
||||
export interface AbilitiesResponse {
|
||||
abilities: Ability[]
|
||||
}
|
||||
|
||||
export const roleService = {
|
||||
async list(params?: ListParams): Promise<ApiResponse<Role[]>> {
|
||||
const { data } = await client.get(API.ROLES, { params })
|
||||
return data
|
||||
},
|
||||
|
||||
async get(id: number): Promise<ApiResponse<Role>> {
|
||||
const { data } = await client.get(`${API.ROLES}/${id}`)
|
||||
return data
|
||||
},
|
||||
|
||||
async create(payload: CreateRolePayload): Promise<{ role: Role }> {
|
||||
const { data } = await client.post(API.ROLES, payload)
|
||||
return data
|
||||
},
|
||||
|
||||
async update(id: number, payload: Partial<CreateRolePayload>): Promise<ApiResponse<Role>> {
|
||||
const { data } = await client.put(`${API.ROLES}/${id}`, payload)
|
||||
return data
|
||||
},
|
||||
|
||||
async delete(id: number): Promise<{ success: boolean }> {
|
||||
const { data } = await client.delete(`${API.ROLES}/${id}`)
|
||||
return data
|
||||
},
|
||||
|
||||
async getAbilities(params?: ListParams): Promise<AbilitiesResponse> {
|
||||
const { data } = await client.get(API.ABILITIES, { params })
|
||||
return data
|
||||
},
|
||||
}
|
||||
109
resources/scripts-v2/api/services/setting.service.ts
Normal file
109
resources/scripts-v2/api/services/setting.service.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import { client } from '../client'
|
||||
import { API } from '../endpoints'
|
||||
import type { Country } from '../../types/domain/customer'
|
||||
import type { Currency } from '../../types/domain/currency'
|
||||
|
||||
export interface DateFormat {
|
||||
display_date: string
|
||||
carbon_format_value: string
|
||||
moment_format_value: string
|
||||
}
|
||||
|
||||
export interface TimeFormat {
|
||||
display_time: string
|
||||
carbon_format_value: string
|
||||
moment_format_value: string
|
||||
}
|
||||
|
||||
export interface ConfigResponse {
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
export interface GlobalSettingsPayload {
|
||||
settings: Record<string, string | number | boolean | null>
|
||||
}
|
||||
|
||||
export interface NumberPlaceholdersParams {
|
||||
key: string
|
||||
}
|
||||
|
||||
export interface NumberPlaceholder {
|
||||
description: string
|
||||
value: string
|
||||
}
|
||||
|
||||
export const settingService = {
|
||||
// Global Settings (admin-level)
|
||||
async getGlobalSettings(): Promise<Record<string, string>> {
|
||||
const { data } = await client.get(API.SETTINGS)
|
||||
return data
|
||||
},
|
||||
|
||||
async updateGlobalSettings(payload: GlobalSettingsPayload): Promise<{ success: boolean }> {
|
||||
const { data } = await client.post(API.SETTINGS, payload)
|
||||
return data
|
||||
},
|
||||
|
||||
// Config
|
||||
async getConfig(params?: Record<string, string>): Promise<ConfigResponse> {
|
||||
const { data } = await client.get(API.CONFIG, { params })
|
||||
return data
|
||||
},
|
||||
|
||||
// Reference Data
|
||||
async getCountries(): Promise<{ data: Country[] }> {
|
||||
const { data } = await client.get(API.COUNTRIES)
|
||||
return data
|
||||
},
|
||||
|
||||
async getCurrencies(): Promise<{ data: Currency[] }> {
|
||||
const { data } = await client.get(API.CURRENCIES)
|
||||
return data
|
||||
},
|
||||
|
||||
async getTimezones(): Promise<{ time_zones: string[] }> {
|
||||
const { data } = await client.get(API.TIMEZONES)
|
||||
return data
|
||||
},
|
||||
|
||||
async getDateFormats(): Promise<{ date_formats: DateFormat[] }> {
|
||||
const { data } = await client.get(API.DATE_FORMATS)
|
||||
return data
|
||||
},
|
||||
|
||||
async getTimeFormats(): Promise<{ time_formats: TimeFormat[] }> {
|
||||
const { data } = await client.get(API.TIME_FORMATS)
|
||||
return data
|
||||
},
|
||||
|
||||
// Serial Numbers
|
||||
async getNextNumber(params: { key: string }): Promise<{ nextNumber: string }> {
|
||||
const { data } = await client.get(API.NEXT_NUMBER, { params })
|
||||
return data
|
||||
},
|
||||
|
||||
async getNumberPlaceholders(params: NumberPlaceholdersParams): Promise<{ placeholders: NumberPlaceholder[] }> {
|
||||
const { data } = await client.get(API.NUMBER_PLACEHOLDERS, { params })
|
||||
return data
|
||||
},
|
||||
|
||||
// Search
|
||||
async search(params: { search: string }): Promise<{
|
||||
users: { data: unknown[] }
|
||||
customers: { data: unknown[] }
|
||||
}> {
|
||||
const { data } = await client.get(API.SEARCH, { params })
|
||||
return data
|
||||
},
|
||||
|
||||
async searchUsers(params: { search: string }): Promise<{ data: unknown[] }> {
|
||||
const { data } = await client.get(API.SEARCH_USERS, { params })
|
||||
return data
|
||||
},
|
||||
|
||||
// App Version
|
||||
async getAppVersion(): Promise<{ version: string }> {
|
||||
const { data } = await client.get(API.APP_VERSION)
|
||||
return data
|
||||
},
|
||||
}
|
||||
41
resources/scripts-v2/api/services/tax-type.service.ts
Normal file
41
resources/scripts-v2/api/services/tax-type.service.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { client } from '../client'
|
||||
import { API } from '../endpoints'
|
||||
import type { TaxType } from '../../types/domain/tax'
|
||||
import type { ApiResponse, ListParams } from '../../types/api'
|
||||
|
||||
export interface CreateTaxTypePayload {
|
||||
name: string
|
||||
percent: number
|
||||
fixed_amount?: number
|
||||
calculation_type?: string | null
|
||||
compound_tax?: boolean
|
||||
collective_tax?: number | null
|
||||
description?: string | null
|
||||
}
|
||||
|
||||
export const taxTypeService = {
|
||||
async list(params?: ListParams): Promise<ApiResponse<TaxType[]>> {
|
||||
const { data } = await client.get(API.TAX_TYPES, { params })
|
||||
return data
|
||||
},
|
||||
|
||||
async get(id: number): Promise<ApiResponse<TaxType>> {
|
||||
const { data } = await client.get(`${API.TAX_TYPES}/${id}`)
|
||||
return data
|
||||
},
|
||||
|
||||
async create(payload: CreateTaxTypePayload): Promise<ApiResponse<TaxType>> {
|
||||
const { data } = await client.post(API.TAX_TYPES, payload)
|
||||
return data
|
||||
},
|
||||
|
||||
async update(id: number, payload: Partial<CreateTaxTypePayload>): Promise<ApiResponse<TaxType>> {
|
||||
const { data } = await client.put(`${API.TAX_TYPES}/${id}`, payload)
|
||||
return data
|
||||
},
|
||||
|
||||
async delete(id: number): Promise<{ success: boolean }> {
|
||||
const { data } = await client.delete(`${API.TAX_TYPES}/${id}`)
|
||||
return data
|
||||
},
|
||||
}
|
||||
48
resources/scripts-v2/api/services/user.service.ts
Normal file
48
resources/scripts-v2/api/services/user.service.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { client } from '../client'
|
||||
import { API } from '../endpoints'
|
||||
import type { User } from '../../types/domain/user'
|
||||
import type { ApiResponse } from '../../types/api'
|
||||
|
||||
export interface UpdateProfilePayload {
|
||||
name: string
|
||||
email: string
|
||||
password?: string | null
|
||||
confirm_password?: string | null
|
||||
}
|
||||
|
||||
export interface UserSettingsPayload {
|
||||
settings: Record<string, string | number | boolean | null>
|
||||
}
|
||||
|
||||
export interface UserSettingsResponse {
|
||||
[key: string]: string | null
|
||||
}
|
||||
|
||||
export const userService = {
|
||||
async getProfile(): Promise<ApiResponse<User>> {
|
||||
const { data } = await client.get(API.ME)
|
||||
return data
|
||||
},
|
||||
|
||||
async updateProfile(payload: UpdateProfilePayload): Promise<ApiResponse<User>> {
|
||||
const { data } = await client.put(API.ME, payload)
|
||||
return data
|
||||
},
|
||||
|
||||
async getSettings(settings?: string[]): Promise<UserSettingsResponse> {
|
||||
const { data } = await client.get(API.ME_SETTINGS, {
|
||||
params: { settings },
|
||||
})
|
||||
return data
|
||||
},
|
||||
|
||||
async updateSettings(payload: UserSettingsPayload): Promise<{ success: boolean }> {
|
||||
const { data } = await client.put(API.ME_SETTINGS, payload)
|
||||
return data
|
||||
},
|
||||
|
||||
async uploadAvatar(payload: FormData): Promise<ApiResponse<User>> {
|
||||
const { data } = await client.post(API.ME_UPLOAD_AVATAR, payload)
|
||||
return data
|
||||
},
|
||||
}
|
||||
54
resources/scripts-v2/composables/index.ts
Normal file
54
resources/scripts-v2/composables/index.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
export { useApi } from './use-api'
|
||||
export type { UseApiReturn } from './use-api'
|
||||
|
||||
export { useAuth } from './use-auth'
|
||||
export type { User, UseAuthReturn } from './use-auth'
|
||||
|
||||
export { useNotification } from './use-notification'
|
||||
export type { Notification, UseNotificationReturn } from './use-notification'
|
||||
|
||||
export { useDialog } from './use-dialog'
|
||||
export type {
|
||||
DialogState,
|
||||
OpenConfirmOptions,
|
||||
UseDialogReturn,
|
||||
} from './use-dialog'
|
||||
|
||||
export { useModal } from './use-modal'
|
||||
export type {
|
||||
ModalState,
|
||||
OpenModalOptions,
|
||||
UseModalReturn,
|
||||
} from './use-modal'
|
||||
|
||||
export { usePagination } from './use-pagination'
|
||||
export type {
|
||||
UsePaginationOptions,
|
||||
UsePaginationReturn,
|
||||
} from './use-pagination'
|
||||
|
||||
export { useFilters } from './use-filters'
|
||||
export type { UseFiltersOptions, UseFiltersReturn } from './use-filters'
|
||||
|
||||
export { useCurrency } from './use-currency'
|
||||
export type { Currency, UseCurrencyReturn } from './use-currency'
|
||||
|
||||
export { useDate } from './use-date'
|
||||
export type { UseDateReturn } from './use-date'
|
||||
|
||||
export { useTheme } from './use-theme'
|
||||
export type { UseThemeReturn } from './use-theme'
|
||||
|
||||
export { useSidebar } from './use-sidebar'
|
||||
export type { UseSidebarReturn } from './use-sidebar'
|
||||
|
||||
export { useCompany } from './use-company'
|
||||
export type {
|
||||
Company,
|
||||
CompanySettings,
|
||||
CompanyCurrency,
|
||||
UseCompanyReturn,
|
||||
} from './use-company'
|
||||
|
||||
export { usePermissions } from './use-permissions'
|
||||
export type { UserAbility, UsePermissionsReturn } from './use-permissions'
|
||||
52
resources/scripts-v2/composables/use-api.ts
Normal file
52
resources/scripts-v2/composables/use-api.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { ref } from 'vue'
|
||||
import type { Ref } from 'vue'
|
||||
import { handleApiError } from '../utils/error-handling'
|
||||
import type { NormalizedApiError } from '../utils/error-handling'
|
||||
|
||||
export interface UseApiReturn<T> {
|
||||
data: Ref<T | null>
|
||||
loading: Ref<boolean>
|
||||
error: Ref<NormalizedApiError | null>
|
||||
execute: (...args: unknown[]) => Promise<T | null>
|
||||
reset: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic API call wrapper composable.
|
||||
* Manages loading, error, and data state for an async API function.
|
||||
*
|
||||
* @param apiFn - An async function that performs the API call
|
||||
* @returns Reactive refs for data, loading, and error plus an execute function
|
||||
*/
|
||||
export function useApi<T>(
|
||||
apiFn: (...args: never[]) => Promise<T>
|
||||
): UseApiReturn<T> {
|
||||
const data = ref<T | null>(null) as Ref<T | null>
|
||||
const loading = ref<boolean>(false)
|
||||
const error = ref<NormalizedApiError | null>(null) as Ref<NormalizedApiError | null>
|
||||
|
||||
async function execute(...args: unknown[]): Promise<T | null> {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
const result = await (apiFn as (...a: unknown[]) => Promise<T>)(...args)
|
||||
data.value = result
|
||||
return result
|
||||
} catch (err: unknown) {
|
||||
const normalized = handleApiError(err)
|
||||
error.value = normalized
|
||||
return null
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function reset(): void {
|
||||
data.value = null
|
||||
loading.value = false
|
||||
error.value = null
|
||||
}
|
||||
|
||||
return { data, loading, error, execute, reset }
|
||||
}
|
||||
86
resources/scripts-v2/composables/use-auth.ts
Normal file
86
resources/scripts-v2/composables/use-auth.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { ref, computed } from 'vue'
|
||||
import type { Ref, ComputedRef } from 'vue'
|
||||
import * as ls from '../utils/local-storage'
|
||||
import { LS_KEYS } from '../config/constants'
|
||||
|
||||
export interface User {
|
||||
id: number
|
||||
name: string
|
||||
email: string
|
||||
avatar: string | number
|
||||
is_owner: boolean
|
||||
is_super_admin: boolean
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
export interface UseAuthReturn {
|
||||
currentUser: Ref<User | null>
|
||||
isAuthenticated: ComputedRef<boolean>
|
||||
isOwner: ComputedRef<boolean>
|
||||
isSuperAdmin: ComputedRef<boolean>
|
||||
setUser: (user: User) => void
|
||||
clearUser: () => void
|
||||
login: (loginFn: () => Promise<User>) => Promise<User>
|
||||
logout: (logoutFn: () => Promise<void>) => Promise<void>
|
||||
}
|
||||
|
||||
const currentUser = ref<User | null>(null)
|
||||
|
||||
/**
|
||||
* Composable for managing authentication state.
|
||||
* Provides the current user, login/logout helpers, and role-based computed properties.
|
||||
*/
|
||||
export function useAuth(): UseAuthReturn {
|
||||
const isAuthenticated = computed<boolean>(() => currentUser.value !== null)
|
||||
|
||||
const isOwner = computed<boolean>(
|
||||
() => currentUser.value?.is_owner === true
|
||||
)
|
||||
|
||||
const isSuperAdmin = computed<boolean>(
|
||||
() => currentUser.value?.is_super_admin === true
|
||||
)
|
||||
|
||||
function setUser(user: User): void {
|
||||
currentUser.value = user
|
||||
}
|
||||
|
||||
function clearUser(): void {
|
||||
currentUser.value = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a login function and set the current user on success.
|
||||
*
|
||||
* @param loginFn - Async function that performs the login and returns a User
|
||||
* @returns The authenticated user
|
||||
*/
|
||||
async function login(loginFn: () => Promise<User>): Promise<User> {
|
||||
const user = await loginFn()
|
||||
currentUser.value = user
|
||||
return user
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a logout function and clear auth state.
|
||||
*
|
||||
* @param logoutFn - Async function that performs the logout
|
||||
*/
|
||||
async function logout(logoutFn: () => Promise<void>): Promise<void> {
|
||||
await logoutFn()
|
||||
currentUser.value = null
|
||||
ls.remove(LS_KEYS.AUTH_TOKEN)
|
||||
ls.remove(LS_KEYS.SELECTED_COMPANY)
|
||||
}
|
||||
|
||||
return {
|
||||
currentUser,
|
||||
isAuthenticated,
|
||||
isOwner,
|
||||
isSuperAdmin,
|
||||
setUser,
|
||||
clearUser,
|
||||
login,
|
||||
logout,
|
||||
}
|
||||
}
|
||||
134
resources/scripts-v2/composables/use-company.ts
Normal file
134
resources/scripts-v2/composables/use-company.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
import { ref, computed } from 'vue'
|
||||
import type { Ref, ComputedRef } from 'vue'
|
||||
import * as ls from '../utils/local-storage'
|
||||
import { LS_KEYS } from '../config/constants'
|
||||
|
||||
export interface Company {
|
||||
id: number
|
||||
unique_hash: string
|
||||
name: string
|
||||
slug: string
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
export interface CompanySettings {
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
export interface CompanyCurrency {
|
||||
id: number
|
||||
code: string
|
||||
name: string
|
||||
symbol: string
|
||||
precision: number
|
||||
thousand_separator: string
|
||||
decimal_separator: string
|
||||
swap_currency_symbol?: boolean
|
||||
exchange_rate?: number
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
export interface UseCompanyReturn {
|
||||
selectedCompany: Ref<Company | null>
|
||||
companies: Ref<Company[]>
|
||||
selectedCompanySettings: Ref<CompanySettings>
|
||||
selectedCompanyCurrency: Ref<CompanyCurrency | null>
|
||||
isAdminMode: Ref<boolean>
|
||||
hasCompany: ComputedRef<boolean>
|
||||
setCompany: (company: Company | null) => void
|
||||
setCompanies: (data: Company[]) => void
|
||||
setCompanySettings: (settings: CompanySettings) => void
|
||||
setCompanyCurrency: (currency: CompanyCurrency | null) => void
|
||||
enterAdminMode: () => void
|
||||
exitAdminMode: () => void
|
||||
}
|
||||
|
||||
const selectedCompany = ref<Company | null>(null)
|
||||
const companies = ref<Company[]>([])
|
||||
const selectedCompanySettings = ref<CompanySettings>({})
|
||||
const selectedCompanyCurrency = ref<CompanyCurrency | null>(null)
|
||||
const isAdminMode = ref<boolean>(
|
||||
ls.get<string>(LS_KEYS.IS_ADMIN_MODE) === 'true'
|
||||
)
|
||||
|
||||
/**
|
||||
* Composable for managing company selection and admin mode state.
|
||||
* Extracted from the Pinia company store, this provides reactive company state
|
||||
* with localStorage persistence for the selected company and admin mode.
|
||||
*/
|
||||
export function useCompany(): UseCompanyReturn {
|
||||
const hasCompany = computed<boolean>(
|
||||
() => selectedCompany.value !== null
|
||||
)
|
||||
|
||||
/**
|
||||
* Set the selected company and persist to localStorage.
|
||||
* Automatically disables admin mode when a company is selected.
|
||||
*
|
||||
* @param company - The company to select, or null to deselect
|
||||
*/
|
||||
function setCompany(company: Company | null): void {
|
||||
if (company) {
|
||||
ls.set(LS_KEYS.SELECTED_COMPANY, String(company.id))
|
||||
ls.remove(LS_KEYS.IS_ADMIN_MODE)
|
||||
isAdminMode.value = false
|
||||
} else {
|
||||
ls.remove(LS_KEYS.SELECTED_COMPANY)
|
||||
}
|
||||
selectedCompany.value = company
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the list of available companies.
|
||||
*/
|
||||
function setCompanies(data: Company[]): void {
|
||||
companies.value = data
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the selected company's settings.
|
||||
*/
|
||||
function setCompanySettings(settings: CompanySettings): void {
|
||||
selectedCompanySettings.value = settings
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the selected company's currency.
|
||||
*/
|
||||
function setCompanyCurrency(currency: CompanyCurrency | null): void {
|
||||
selectedCompanyCurrency.value = currency
|
||||
}
|
||||
|
||||
/**
|
||||
* Enter admin mode. Clears the selected company and persists admin mode.
|
||||
*/
|
||||
function enterAdminMode(): void {
|
||||
isAdminMode.value = true
|
||||
ls.set(LS_KEYS.IS_ADMIN_MODE, 'true')
|
||||
ls.remove(LS_KEYS.SELECTED_COMPANY)
|
||||
selectedCompany.value = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Exit admin mode.
|
||||
*/
|
||||
function exitAdminMode(): void {
|
||||
isAdminMode.value = false
|
||||
ls.remove(LS_KEYS.IS_ADMIN_MODE)
|
||||
}
|
||||
|
||||
return {
|
||||
selectedCompany,
|
||||
companies,
|
||||
selectedCompanySettings,
|
||||
selectedCompanyCurrency,
|
||||
isAdminMode,
|
||||
hasCompany,
|
||||
setCompany,
|
||||
setCompanies,
|
||||
setCompanySettings,
|
||||
setCompanyCurrency,
|
||||
enterAdminMode,
|
||||
exitAdminMode,
|
||||
}
|
||||
}
|
||||
105
resources/scripts-v2/composables/use-currency.ts
Normal file
105
resources/scripts-v2/composables/use-currency.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import { ref } from 'vue'
|
||||
import type { Ref } from 'vue'
|
||||
import { formatMoney as formatMoneyUtil } from '../utils/format-money'
|
||||
import type { CurrencyConfig } from '../utils/format-money'
|
||||
|
||||
export interface Currency {
|
||||
id: number
|
||||
code: string
|
||||
name: string
|
||||
precision: number
|
||||
thousand_separator: string
|
||||
decimal_separator: string
|
||||
symbol: string
|
||||
swap_currency_symbol?: boolean
|
||||
exchange_rate?: number
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
export interface UseCurrencyReturn {
|
||||
currencies: Ref<Currency[]>
|
||||
setCurrencies: (data: Currency[]) => void
|
||||
formatMoney: (amountInCents: number, currency?: CurrencyConfig) => string
|
||||
convertCurrency: (
|
||||
amountInCents: number,
|
||||
fromRate: number,
|
||||
toRate: number
|
||||
) => number
|
||||
findCurrencyByCode: (code: string) => Currency | undefined
|
||||
}
|
||||
|
||||
const currencies = ref<Currency[]>([])
|
||||
|
||||
/**
|
||||
* Default currency configuration matching the v1 behavior.
|
||||
*/
|
||||
const DEFAULT_CURRENCY_CONFIG: CurrencyConfig = {
|
||||
precision: 2,
|
||||
thousand_separator: ',',
|
||||
decimal_separator: '.',
|
||||
symbol: '$',
|
||||
swap_currency_symbol: false,
|
||||
}
|
||||
|
||||
/**
|
||||
* Composable for currency formatting and conversion.
|
||||
* Maintains a shared list of available currencies.
|
||||
*/
|
||||
export function useCurrency(): UseCurrencyReturn {
|
||||
function setCurrencies(data: Currency[]): void {
|
||||
currencies.value = data
|
||||
}
|
||||
|
||||
/**
|
||||
* Format an amount in cents using the provided or default currency config.
|
||||
*
|
||||
* @param amountInCents - Amount in cents
|
||||
* @param currency - Optional currency config override
|
||||
* @returns Formatted currency string
|
||||
*/
|
||||
function formatMoney(
|
||||
amountInCents: number,
|
||||
currency?: CurrencyConfig
|
||||
): string {
|
||||
return formatMoneyUtil(amountInCents, currency ?? DEFAULT_CURRENCY_CONFIG)
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an amount from one currency to another using exchange rates.
|
||||
*
|
||||
* @param amountInCents - Amount in cents in the source currency
|
||||
* @param fromRate - Exchange rate of the source currency
|
||||
* @param toRate - Exchange rate of the target currency
|
||||
* @returns Converted amount in cents
|
||||
*/
|
||||
function convertCurrency(
|
||||
amountInCents: number,
|
||||
fromRate: number,
|
||||
toRate: number
|
||||
): number {
|
||||
if (fromRate === 0) {
|
||||
return 0
|
||||
}
|
||||
return Math.round((amountInCents / fromRate) * toRate)
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a currency by its ISO code.
|
||||
*
|
||||
* @param code - The ISO 4217 currency code (e.g. "USD")
|
||||
* @returns The matching Currency, or undefined
|
||||
*/
|
||||
function findCurrencyByCode(code: string): Currency | undefined {
|
||||
return currencies.value.find(
|
||||
(c) => c.code.toUpperCase() === code.toUpperCase()
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
currencies,
|
||||
setCurrencies,
|
||||
formatMoney,
|
||||
convertCurrency,
|
||||
findCurrencyByCode,
|
||||
}
|
||||
}
|
||||
92
resources/scripts-v2/composables/use-date.ts
Normal file
92
resources/scripts-v2/composables/use-date.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import {
|
||||
formatDate as formatDateUtil,
|
||||
relativeTime as relativeTimeUtil,
|
||||
parseDate as parseDateUtil,
|
||||
DEFAULT_DATE_FORMAT,
|
||||
DEFAULT_DATETIME_FORMAT,
|
||||
} from '../utils/format-date'
|
||||
import type { Locale } from 'date-fns'
|
||||
|
||||
export interface UseDateReturn {
|
||||
formatDate: (
|
||||
date: Date | string | number,
|
||||
formatStr?: string,
|
||||
options?: { locale?: Locale }
|
||||
) => string
|
||||
formatDateTime: (
|
||||
date: Date | string | number,
|
||||
options?: { locale?: Locale }
|
||||
) => string
|
||||
relativeTime: (
|
||||
date: Date | string | number,
|
||||
options?: { addSuffix?: boolean; locale?: Locale }
|
||||
) => string
|
||||
parseDate: (date: Date | string | number) => Date | null
|
||||
}
|
||||
|
||||
/**
|
||||
* Composable for date formatting and parsing using date-fns.
|
||||
* Provides convenient wrappers around utility functions for use within Vue components.
|
||||
*/
|
||||
export function useDate(): UseDateReturn {
|
||||
/**
|
||||
* Format a date using a format pattern.
|
||||
*
|
||||
* @param date - A Date object, ISO string, or timestamp
|
||||
* @param formatStr - date-fns format pattern (default: 'yyyy-MM-dd')
|
||||
* @param options - Optional locale
|
||||
* @returns Formatted date string, or empty string if invalid
|
||||
*/
|
||||
function formatDate(
|
||||
date: Date | string | number,
|
||||
formatStr: string = DEFAULT_DATE_FORMAT,
|
||||
options?: { locale?: Locale }
|
||||
): string {
|
||||
return formatDateUtil(date, formatStr, options)
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a date with date and time.
|
||||
*
|
||||
* @param date - A Date object, ISO string, or timestamp
|
||||
* @param options - Optional locale
|
||||
* @returns Formatted datetime string
|
||||
*/
|
||||
function formatDateTime(
|
||||
date: Date | string | number,
|
||||
options?: { locale?: Locale }
|
||||
): string {
|
||||
return formatDateUtil(date, DEFAULT_DATETIME_FORMAT, options)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a human-readable relative time string (e.g. "3 days ago").
|
||||
*
|
||||
* @param date - A Date object, ISO string, or timestamp
|
||||
* @param options - Optional addSuffix and locale settings
|
||||
* @returns Relative time string
|
||||
*/
|
||||
function relativeTime(
|
||||
date: Date | string | number,
|
||||
options?: { addSuffix?: boolean; locale?: Locale }
|
||||
): string {
|
||||
return relativeTimeUtil(date, options)
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a date value into a Date object.
|
||||
*
|
||||
* @param date - A Date object, ISO string, or timestamp
|
||||
* @returns Parsed Date or null
|
||||
*/
|
||||
function parseDate(date: Date | string | number): Date | null {
|
||||
return parseDateUtil(date)
|
||||
}
|
||||
|
||||
return {
|
||||
formatDate,
|
||||
formatDateTime,
|
||||
relativeTime,
|
||||
parseDate,
|
||||
}
|
||||
}
|
||||
113
resources/scripts-v2/composables/use-dialog.ts
Normal file
113
resources/scripts-v2/composables/use-dialog.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import { ref, readonly } from 'vue'
|
||||
import type { DeepReadonly, Ref } from 'vue'
|
||||
import { DIALOG_VARIANT } from '../config/constants'
|
||||
import type { DialogVariant } from '../config/constants'
|
||||
|
||||
export interface DialogState {
|
||||
active: boolean
|
||||
title: string
|
||||
message: string
|
||||
variant: DialogVariant
|
||||
yesLabel: string
|
||||
noLabel: string
|
||||
hideNoButton: boolean
|
||||
data: unknown
|
||||
}
|
||||
|
||||
export interface OpenConfirmOptions {
|
||||
title: string
|
||||
message: string
|
||||
variant?: DialogVariant
|
||||
yesLabel?: string
|
||||
noLabel?: string
|
||||
hideNoButton?: boolean
|
||||
data?: unknown
|
||||
}
|
||||
|
||||
export interface UseDialogReturn {
|
||||
dialogState: DeepReadonly<Ref<DialogState>>
|
||||
openConfirm: (options: OpenConfirmOptions) => Promise<boolean>
|
||||
closeDialog: () => void
|
||||
confirmDialog: () => void
|
||||
cancelDialog: () => void
|
||||
}
|
||||
|
||||
const DEFAULT_STATE: DialogState = {
|
||||
active: false,
|
||||
title: '',
|
||||
message: '',
|
||||
variant: DIALOG_VARIANT.DANGER,
|
||||
yesLabel: 'Yes',
|
||||
noLabel: 'No',
|
||||
hideNoButton: false,
|
||||
data: null,
|
||||
}
|
||||
|
||||
const dialogState = ref<DialogState>({ ...DEFAULT_STATE })
|
||||
|
||||
let resolvePromise: ((value: boolean) => void) | null = null
|
||||
|
||||
/**
|
||||
* Composable for managing confirmation dialogs.
|
||||
* Returns a promise that resolves to true (confirmed) or false (cancelled).
|
||||
*/
|
||||
export function useDialog(): UseDialogReturn {
|
||||
/**
|
||||
* Open a confirmation dialog and await the user's response.
|
||||
*
|
||||
* @param options - Dialog configuration
|
||||
* @returns Promise that resolves to true if confirmed, false if cancelled
|
||||
*/
|
||||
function openConfirm(options: OpenConfirmOptions): Promise<boolean> {
|
||||
dialogState.value = {
|
||||
active: true,
|
||||
title: options.title,
|
||||
message: options.message,
|
||||
variant: options.variant ?? DIALOG_VARIANT.DANGER,
|
||||
yesLabel: options.yesLabel ?? 'Yes',
|
||||
noLabel: options.noLabel ?? 'No',
|
||||
hideNoButton: options.hideNoButton ?? false,
|
||||
data: options.data ?? null,
|
||||
}
|
||||
|
||||
return new Promise<boolean>((resolve) => {
|
||||
resolvePromise = resolve
|
||||
})
|
||||
}
|
||||
|
||||
function confirmDialog(): void {
|
||||
if (resolvePromise) {
|
||||
resolvePromise(true)
|
||||
resolvePromise = null
|
||||
}
|
||||
resetDialog()
|
||||
}
|
||||
|
||||
function cancelDialog(): void {
|
||||
if (resolvePromise) {
|
||||
resolvePromise(false)
|
||||
resolvePromise = null
|
||||
}
|
||||
resetDialog()
|
||||
}
|
||||
|
||||
function closeDialog(): void {
|
||||
cancelDialog()
|
||||
}
|
||||
|
||||
function resetDialog(): void {
|
||||
dialogState.value.active = false
|
||||
|
||||
setTimeout(() => {
|
||||
dialogState.value = { ...DEFAULT_STATE }
|
||||
}, 300)
|
||||
}
|
||||
|
||||
return {
|
||||
dialogState: readonly(dialogState),
|
||||
openConfirm,
|
||||
closeDialog,
|
||||
confirmDialog,
|
||||
cancelDialog,
|
||||
}
|
||||
}
|
||||
105
resources/scripts-v2/composables/use-filters.ts
Normal file
105
resources/scripts-v2/composables/use-filters.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import { reactive, ref, watch } from 'vue'
|
||||
import type { Ref } from 'vue'
|
||||
|
||||
export interface UseFiltersOptions<T extends Record<string, unknown>> {
|
||||
initialFilters: T
|
||||
debounceMs?: number
|
||||
onChange?: (filters: T) => void
|
||||
}
|
||||
|
||||
export interface UseFiltersReturn<T extends Record<string, unknown>> {
|
||||
filters: T
|
||||
activeFilterCount: Ref<number>
|
||||
setFilter: <K extends keyof T>(key: K, value: T[K]) => void
|
||||
clearFilters: () => void
|
||||
clearFilter: <K extends keyof T>(key: K) => void
|
||||
getFiltersSnapshot: () => T
|
||||
}
|
||||
|
||||
/**
|
||||
* Composable for managing list filter state with optional debounced apply.
|
||||
*
|
||||
* @param options - Configuration including initial filter values, debounce delay, and change callback
|
||||
*/
|
||||
export function useFilters<T extends Record<string, unknown>>(
|
||||
options: UseFiltersOptions<T>
|
||||
): UseFiltersReturn<T> {
|
||||
const { initialFilters, debounceMs = 300, onChange } = options
|
||||
|
||||
const filters = reactive<T>({ ...initialFilters }) as T
|
||||
|
||||
const activeFilterCount = ref<number>(0)
|
||||
|
||||
let debounceTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
function updateActiveFilterCount(): void {
|
||||
let count = 0
|
||||
const initialKeys = Object.keys(initialFilters) as Array<keyof T>
|
||||
|
||||
for (const key of initialKeys) {
|
||||
const current = filters[key]
|
||||
const initial = initialFilters[key]
|
||||
|
||||
if (current !== initial && current !== '' && current !== null && current !== undefined) {
|
||||
count++
|
||||
}
|
||||
}
|
||||
|
||||
activeFilterCount.value = count
|
||||
}
|
||||
|
||||
function debouncedApply(): void {
|
||||
if (debounceTimer) {
|
||||
clearTimeout(debounceTimer)
|
||||
}
|
||||
|
||||
debounceTimer = setTimeout(() => {
|
||||
updateActiveFilterCount()
|
||||
onChange?.({ ...filters })
|
||||
}, debounceMs)
|
||||
}
|
||||
|
||||
function setFilter<K extends keyof T>(key: K, value: T[K]): void {
|
||||
filters[key] = value
|
||||
debouncedApply()
|
||||
}
|
||||
|
||||
function clearFilter<K extends keyof T>(key: K): void {
|
||||
filters[key] = initialFilters[key]
|
||||
debouncedApply()
|
||||
}
|
||||
|
||||
function clearFilters(): void {
|
||||
const keys = Object.keys(initialFilters) as Array<keyof T>
|
||||
for (const key of keys) {
|
||||
filters[key] = initialFilters[key]
|
||||
}
|
||||
updateActiveFilterCount()
|
||||
onChange?.({ ...filters })
|
||||
}
|
||||
|
||||
function getFiltersSnapshot(): T {
|
||||
return { ...filters }
|
||||
}
|
||||
|
||||
// Watch for external reactive changes and apply debounce
|
||||
watch(
|
||||
() => ({ ...filters }),
|
||||
() => {
|
||||
debouncedApply()
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
// Initialize the active count
|
||||
updateActiveFilterCount()
|
||||
|
||||
return {
|
||||
filters,
|
||||
activeFilterCount,
|
||||
setFilter,
|
||||
clearFilters,
|
||||
clearFilter,
|
||||
getFiltersSnapshot,
|
||||
}
|
||||
}
|
||||
101
resources/scripts-v2/composables/use-modal.ts
Normal file
101
resources/scripts-v2/composables/use-modal.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import { ref, computed, readonly } from 'vue'
|
||||
import type { DeepReadonly, Ref, ComputedRef } from 'vue'
|
||||
import type { ModalSize } from '../config/constants'
|
||||
|
||||
export interface ModalState {
|
||||
active: boolean
|
||||
componentName: string
|
||||
title: string
|
||||
content: string
|
||||
id: string
|
||||
size: ModalSize
|
||||
data: unknown
|
||||
refreshData: (() => void) | null
|
||||
variant: string
|
||||
}
|
||||
|
||||
export interface OpenModalOptions {
|
||||
componentName: string
|
||||
title?: string
|
||||
content?: string
|
||||
id?: string
|
||||
size?: ModalSize
|
||||
data?: unknown
|
||||
refreshData?: () => void
|
||||
variant?: string
|
||||
}
|
||||
|
||||
export interface UseModalReturn {
|
||||
modalState: DeepReadonly<Ref<ModalState>>
|
||||
isEdit: ComputedRef<boolean>
|
||||
openModal: (options: OpenModalOptions) => void
|
||||
closeModal: () => void
|
||||
resetModalData: () => void
|
||||
}
|
||||
|
||||
const DEFAULT_STATE: ModalState = {
|
||||
active: false,
|
||||
componentName: '',
|
||||
title: '',
|
||||
content: '',
|
||||
id: '',
|
||||
size: 'md',
|
||||
data: null,
|
||||
refreshData: null,
|
||||
variant: '',
|
||||
}
|
||||
|
||||
const modalState = ref<ModalState>({ ...DEFAULT_STATE })
|
||||
|
||||
/**
|
||||
* Composable for managing a global modal.
|
||||
* Supports opening modals by component name with props, and tracks edit vs create mode.
|
||||
*/
|
||||
export function useModal(): UseModalReturn {
|
||||
const isEdit = computed<boolean>(() => modalState.value.id !== '')
|
||||
|
||||
/**
|
||||
* Open a modal with the specified options.
|
||||
*
|
||||
* @param options - Modal configuration including component name and optional props
|
||||
*/
|
||||
function openModal(options: OpenModalOptions): void {
|
||||
modalState.value = {
|
||||
active: true,
|
||||
componentName: options.componentName,
|
||||
title: options.title ?? '',
|
||||
content: options.content ?? '',
|
||||
id: options.id ?? '',
|
||||
size: options.size ?? 'md',
|
||||
data: options.data ?? null,
|
||||
refreshData: options.refreshData ?? null,
|
||||
variant: options.variant ?? '',
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the modal with a brief delay for animation.
|
||||
*/
|
||||
function closeModal(): void {
|
||||
modalState.value.active = false
|
||||
|
||||
setTimeout(() => {
|
||||
resetModalData()
|
||||
}, 300)
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset modal data back to defaults.
|
||||
*/
|
||||
function resetModalData(): void {
|
||||
modalState.value = { ...DEFAULT_STATE }
|
||||
}
|
||||
|
||||
return {
|
||||
modalState: readonly(modalState),
|
||||
isEdit,
|
||||
openModal,
|
||||
closeModal,
|
||||
resetModalData,
|
||||
}
|
||||
}
|
||||
83
resources/scripts-v2/composables/use-notification.ts
Normal file
83
resources/scripts-v2/composables/use-notification.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { ref, readonly } from 'vue'
|
||||
import type { DeepReadonly, Ref } from 'vue'
|
||||
import { NOTIFICATION_TYPE } from '../config/constants'
|
||||
import type { NotificationType } from '../config/constants'
|
||||
|
||||
export interface Notification {
|
||||
id: string
|
||||
type: NotificationType
|
||||
message: string
|
||||
}
|
||||
|
||||
export interface UseNotificationReturn {
|
||||
notifications: DeepReadonly<Ref<Notification[]>>
|
||||
showSuccess: (message: string) => void
|
||||
showError: (message: string) => void
|
||||
showInfo: (message: string) => void
|
||||
showWarning: (message: string) => void
|
||||
showNotification: (type: NotificationType, message: string) => void
|
||||
hideNotification: (id: string) => void
|
||||
clearAll: () => void
|
||||
}
|
||||
|
||||
const notifications = ref<Notification[]>([])
|
||||
|
||||
/**
|
||||
* Generate a unique ID for a notification.
|
||||
*/
|
||||
function generateId(): string {
|
||||
return (
|
||||
Math.random().toString(36) + Date.now().toString(36)
|
||||
).substring(2)
|
||||
}
|
||||
|
||||
/**
|
||||
* Composable for managing application-wide notifications.
|
||||
* Provides typed helpers for success, error, info, and warning notifications.
|
||||
*/
|
||||
export function useNotification(): UseNotificationReturn {
|
||||
function showNotification(type: NotificationType, message: string): void {
|
||||
notifications.value.push({
|
||||
id: generateId(),
|
||||
type,
|
||||
message,
|
||||
})
|
||||
}
|
||||
|
||||
function showSuccess(message: string): void {
|
||||
showNotification(NOTIFICATION_TYPE.SUCCESS, message)
|
||||
}
|
||||
|
||||
function showError(message: string): void {
|
||||
showNotification(NOTIFICATION_TYPE.ERROR, message)
|
||||
}
|
||||
|
||||
function showInfo(message: string): void {
|
||||
showNotification(NOTIFICATION_TYPE.INFO, message)
|
||||
}
|
||||
|
||||
function showWarning(message: string): void {
|
||||
showNotification(NOTIFICATION_TYPE.WARNING, message)
|
||||
}
|
||||
|
||||
function hideNotification(id: string): void {
|
||||
notifications.value = notifications.value.filter(
|
||||
(notification) => notification.id !== id
|
||||
)
|
||||
}
|
||||
|
||||
function clearAll(): void {
|
||||
notifications.value = []
|
||||
}
|
||||
|
||||
return {
|
||||
notifications: readonly(notifications),
|
||||
showSuccess,
|
||||
showError,
|
||||
showInfo,
|
||||
showWarning,
|
||||
showNotification,
|
||||
hideNotification,
|
||||
clearAll,
|
||||
}
|
||||
}
|
||||
91
resources/scripts-v2/composables/use-pagination.ts
Normal file
91
resources/scripts-v2/composables/use-pagination.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import { ref, computed } from 'vue'
|
||||
import type { Ref, ComputedRef } from 'vue'
|
||||
import { PAGINATION_DEFAULTS } from '../config/constants'
|
||||
|
||||
export interface UsePaginationOptions {
|
||||
initialPage?: number
|
||||
initialLimit?: number
|
||||
}
|
||||
|
||||
export interface UsePaginationReturn {
|
||||
page: Ref<number>
|
||||
limit: Ref<number>
|
||||
totalCount: Ref<number>
|
||||
totalPages: ComputedRef<number>
|
||||
hasNextPage: ComputedRef<boolean>
|
||||
hasPrevPage: ComputedRef<boolean>
|
||||
nextPage: () => void
|
||||
prevPage: () => void
|
||||
goToPage: (target: number) => void
|
||||
setTotalCount: (count: number) => void
|
||||
reset: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Composable for managing pagination state.
|
||||
* Tracks page, limit, total count, and provides navigation helpers.
|
||||
*
|
||||
* @param options - Optional initial page and limit values
|
||||
*/
|
||||
export function usePagination(
|
||||
options?: UsePaginationOptions
|
||||
): UsePaginationReturn {
|
||||
const initialPage = options?.initialPage ?? PAGINATION_DEFAULTS.PAGE
|
||||
const initialLimit = options?.initialLimit ?? PAGINATION_DEFAULTS.LIMIT
|
||||
|
||||
const page = ref<number>(initialPage)
|
||||
const limit = ref<number>(initialLimit)
|
||||
const totalCount = ref<number>(0)
|
||||
|
||||
const totalPages = computed<number>(() => {
|
||||
if (totalCount.value === 0 || limit.value === 0) {
|
||||
return 0
|
||||
}
|
||||
return Math.ceil(totalCount.value / limit.value)
|
||||
})
|
||||
|
||||
const hasNextPage = computed<boolean>(() => page.value < totalPages.value)
|
||||
|
||||
const hasPrevPage = computed<boolean>(() => page.value > 1)
|
||||
|
||||
function nextPage(): void {
|
||||
if (hasNextPage.value) {
|
||||
page.value += 1
|
||||
}
|
||||
}
|
||||
|
||||
function prevPage(): void {
|
||||
if (hasPrevPage.value) {
|
||||
page.value -= 1
|
||||
}
|
||||
}
|
||||
|
||||
function goToPage(target: number): void {
|
||||
if (target >= 1 && target <= totalPages.value) {
|
||||
page.value = target
|
||||
}
|
||||
}
|
||||
|
||||
function setTotalCount(count: number): void {
|
||||
totalCount.value = count
|
||||
}
|
||||
|
||||
function reset(): void {
|
||||
page.value = initialPage
|
||||
totalCount.value = 0
|
||||
}
|
||||
|
||||
return {
|
||||
page,
|
||||
limit,
|
||||
totalCount,
|
||||
totalPages,
|
||||
hasNextPage,
|
||||
hasPrevPage,
|
||||
nextPage,
|
||||
prevPage,
|
||||
goToPage,
|
||||
setTotalCount,
|
||||
reset,
|
||||
}
|
||||
}
|
||||
160
resources/scripts-v2/composables/use-permissions.ts
Normal file
160
resources/scripts-v2/composables/use-permissions.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
import { ref, computed } from 'vue'
|
||||
import type { Ref, ComputedRef } from 'vue'
|
||||
import { ABILITIES } from '../config/abilities'
|
||||
import type { Ability } from '../config/abilities'
|
||||
|
||||
export interface UserAbility {
|
||||
name: string
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
export interface UsePermissionsReturn {
|
||||
currentAbilities: Ref<UserAbility[]>
|
||||
isOwner: Ref<boolean>
|
||||
isSuperAdmin: Ref<boolean>
|
||||
setAbilities: (abilities: UserAbility[]) => void
|
||||
setOwner: (owner: boolean) => void
|
||||
setSuperAdmin: (superAdmin: boolean) => void
|
||||
hasAbility: (ability: Ability | Ability[]) => boolean
|
||||
hasAllAbilities: (abilities: Ability[]) => boolean
|
||||
canViewCustomer: ComputedRef<boolean>
|
||||
canCreateCustomer: ComputedRef<boolean>
|
||||
canEditCustomer: ComputedRef<boolean>
|
||||
canDeleteCustomer: ComputedRef<boolean>
|
||||
canViewInvoice: ComputedRef<boolean>
|
||||
canCreateInvoice: ComputedRef<boolean>
|
||||
canEditInvoice: ComputedRef<boolean>
|
||||
canDeleteInvoice: ComputedRef<boolean>
|
||||
canViewEstimate: ComputedRef<boolean>
|
||||
canCreateEstimate: ComputedRef<boolean>
|
||||
canEditEstimate: ComputedRef<boolean>
|
||||
canDeleteEstimate: ComputedRef<boolean>
|
||||
canViewPayment: ComputedRef<boolean>
|
||||
canCreatePayment: ComputedRef<boolean>
|
||||
canEditPayment: ComputedRef<boolean>
|
||||
canDeletePayment: ComputedRef<boolean>
|
||||
canViewExpense: ComputedRef<boolean>
|
||||
canCreateExpense: ComputedRef<boolean>
|
||||
canEditExpense: ComputedRef<boolean>
|
||||
canDeleteExpense: ComputedRef<boolean>
|
||||
canViewDashboard: ComputedRef<boolean>
|
||||
canViewFinancialReport: ComputedRef<boolean>
|
||||
}
|
||||
|
||||
const currentAbilities = ref<UserAbility[]>([])
|
||||
const isOwner = ref<boolean>(false)
|
||||
const isSuperAdmin = ref<boolean>(false)
|
||||
|
||||
/**
|
||||
* Composable for managing user permissions and abilities.
|
||||
* Extracted from the user store's hasAbilities/hasAllAbilities logic,
|
||||
* with typed convenience computed properties for common CRUD checks.
|
||||
*/
|
||||
export function usePermissions(): UsePermissionsReturn {
|
||||
function setAbilities(abilities: UserAbility[]): void {
|
||||
currentAbilities.value = abilities
|
||||
}
|
||||
|
||||
function setOwner(owner: boolean): void {
|
||||
isOwner.value = owner
|
||||
}
|
||||
|
||||
function setSuperAdmin(superAdmin: boolean): void {
|
||||
isSuperAdmin.value = superAdmin
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current user has a given ability or any of the given abilities.
|
||||
* A wildcard ability ('*') grants access to everything.
|
||||
*
|
||||
* @param ability - A single ability string or array of abilities
|
||||
* @returns True if the user has the ability
|
||||
*/
|
||||
function hasAbility(ability: Ability | Ability[]): boolean {
|
||||
return !!currentAbilities.value.find((ab) => {
|
||||
if (ab.name === '*') return true
|
||||
|
||||
if (typeof ability === 'string') {
|
||||
return ab.name === ability
|
||||
}
|
||||
|
||||
return !!ability.find((p) => ab.name === p)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current user has ALL of the given abilities.
|
||||
*
|
||||
* @param abilities - Array of abilities that must all be present
|
||||
* @returns True if the user has every listed ability
|
||||
*/
|
||||
function hasAllAbilities(abilities: Ability[]): boolean {
|
||||
return abilities.every((ability) =>
|
||||
currentAbilities.value.some(
|
||||
(ab) => ab.name === '*' || ab.name === ability
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// Convenience computed properties for common permission checks
|
||||
const canViewCustomer = computed<boolean>(() => hasAbility(ABILITIES.VIEW_CUSTOMER))
|
||||
const canCreateCustomer = computed<boolean>(() => hasAbility(ABILITIES.CREATE_CUSTOMER))
|
||||
const canEditCustomer = computed<boolean>(() => hasAbility(ABILITIES.EDIT_CUSTOMER))
|
||||
const canDeleteCustomer = computed<boolean>(() => hasAbility(ABILITIES.DELETE_CUSTOMER))
|
||||
|
||||
const canViewInvoice = computed<boolean>(() => hasAbility(ABILITIES.VIEW_INVOICE))
|
||||
const canCreateInvoice = computed<boolean>(() => hasAbility(ABILITIES.CREATE_INVOICE))
|
||||
const canEditInvoice = computed<boolean>(() => hasAbility(ABILITIES.EDIT_INVOICE))
|
||||
const canDeleteInvoice = computed<boolean>(() => hasAbility(ABILITIES.DELETE_INVOICE))
|
||||
|
||||
const canViewEstimate = computed<boolean>(() => hasAbility(ABILITIES.VIEW_ESTIMATE))
|
||||
const canCreateEstimate = computed<boolean>(() => hasAbility(ABILITIES.CREATE_ESTIMATE))
|
||||
const canEditEstimate = computed<boolean>(() => hasAbility(ABILITIES.EDIT_ESTIMATE))
|
||||
const canDeleteEstimate = computed<boolean>(() => hasAbility(ABILITIES.DELETE_ESTIMATE))
|
||||
|
||||
const canViewPayment = computed<boolean>(() => hasAbility(ABILITIES.VIEW_PAYMENT))
|
||||
const canCreatePayment = computed<boolean>(() => hasAbility(ABILITIES.CREATE_PAYMENT))
|
||||
const canEditPayment = computed<boolean>(() => hasAbility(ABILITIES.EDIT_PAYMENT))
|
||||
const canDeletePayment = computed<boolean>(() => hasAbility(ABILITIES.DELETE_PAYMENT))
|
||||
|
||||
const canViewExpense = computed<boolean>(() => hasAbility(ABILITIES.VIEW_EXPENSE))
|
||||
const canCreateExpense = computed<boolean>(() => hasAbility(ABILITIES.CREATE_EXPENSE))
|
||||
const canEditExpense = computed<boolean>(() => hasAbility(ABILITIES.EDIT_EXPENSE))
|
||||
const canDeleteExpense = computed<boolean>(() => hasAbility(ABILITIES.DELETE_EXPENSE))
|
||||
|
||||
const canViewDashboard = computed<boolean>(() => hasAbility(ABILITIES.DASHBOARD))
|
||||
const canViewFinancialReport = computed<boolean>(() => hasAbility(ABILITIES.VIEW_FINANCIAL_REPORT))
|
||||
|
||||
return {
|
||||
currentAbilities,
|
||||
isOwner,
|
||||
isSuperAdmin,
|
||||
setAbilities,
|
||||
setOwner,
|
||||
setSuperAdmin,
|
||||
hasAbility,
|
||||
hasAllAbilities,
|
||||
canViewCustomer,
|
||||
canCreateCustomer,
|
||||
canEditCustomer,
|
||||
canDeleteCustomer,
|
||||
canViewInvoice,
|
||||
canCreateInvoice,
|
||||
canEditInvoice,
|
||||
canDeleteInvoice,
|
||||
canViewEstimate,
|
||||
canCreateEstimate,
|
||||
canEditEstimate,
|
||||
canDeleteEstimate,
|
||||
canViewPayment,
|
||||
canCreatePayment,
|
||||
canEditPayment,
|
||||
canDeletePayment,
|
||||
canViewExpense,
|
||||
canCreateExpense,
|
||||
canEditExpense,
|
||||
canDeleteExpense,
|
||||
canViewDashboard,
|
||||
canViewFinancialReport,
|
||||
}
|
||||
}
|
||||
49
resources/scripts-v2/composables/use-sidebar.ts
Normal file
49
resources/scripts-v2/composables/use-sidebar.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { ref } from 'vue'
|
||||
import type { Ref } from 'vue'
|
||||
import * as ls from '../utils/local-storage'
|
||||
import { LS_KEYS } from '../config/constants'
|
||||
|
||||
export interface UseSidebarReturn {
|
||||
isCollapsed: Ref<boolean>
|
||||
isSidebarOpen: Ref<boolean>
|
||||
toggleCollapse: () => void
|
||||
setSidebarVisibility: (visible: boolean) => void
|
||||
}
|
||||
|
||||
const isCollapsed = ref<boolean>(
|
||||
ls.get<string>(LS_KEYS.SIDEBAR_COLLAPSED) === 'true'
|
||||
)
|
||||
|
||||
const isSidebarOpen = ref<boolean>(false)
|
||||
|
||||
/**
|
||||
* Composable for managing sidebar state.
|
||||
* Handles both the mobile sidebar open/close and the desktop collapsed toggle.
|
||||
* Persists the collapsed state to localStorage.
|
||||
*/
|
||||
export function useSidebar(): UseSidebarReturn {
|
||||
/**
|
||||
* Toggle the sidebar collapsed/expanded state (desktop).
|
||||
* Persists the new value to localStorage.
|
||||
*/
|
||||
function toggleCollapse(): void {
|
||||
isCollapsed.value = !isCollapsed.value
|
||||
ls.set(LS_KEYS.SIDEBAR_COLLAPSED, isCollapsed.value ? 'true' : 'false')
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the mobile sidebar visibility.
|
||||
*
|
||||
* @param visible - Whether the sidebar should be visible
|
||||
*/
|
||||
function setSidebarVisibility(visible: boolean): void {
|
||||
isSidebarOpen.value = visible
|
||||
}
|
||||
|
||||
return {
|
||||
isCollapsed,
|
||||
isSidebarOpen,
|
||||
toggleCollapse,
|
||||
setSidebarVisibility,
|
||||
}
|
||||
}
|
||||
92
resources/scripts-v2/composables/use-theme.ts
Normal file
92
resources/scripts-v2/composables/use-theme.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import type { Ref } from 'vue'
|
||||
import { THEME, LS_KEYS } from '../config/constants'
|
||||
import type { Theme } from '../config/constants'
|
||||
import * as ls from '../utils/local-storage'
|
||||
|
||||
export interface UseThemeReturn {
|
||||
currentTheme: Ref<Theme>
|
||||
setTheme: (theme: Theme) => void
|
||||
applyTheme: (theme?: Theme) => void
|
||||
}
|
||||
|
||||
const currentTheme = ref<Theme>(
|
||||
(ls.get<string>(LS_KEYS.THEME) as Theme) ?? THEME.SYSTEM
|
||||
)
|
||||
|
||||
let mediaQueryCleanup: (() => void) | null = null
|
||||
|
||||
/**
|
||||
* Apply the correct data-theme attribute to the document element.
|
||||
*
|
||||
* @param theme - The theme to apply (light, dark, or system)
|
||||
*/
|
||||
function applyThemeToDocument(theme: Theme): void {
|
||||
const prefersDark =
|
||||
theme === THEME.DARK ||
|
||||
(theme === THEME.SYSTEM &&
|
||||
window.matchMedia('(prefers-color-scheme: dark)').matches)
|
||||
|
||||
if (prefersDark) {
|
||||
document.documentElement.setAttribute('data-theme', 'dark')
|
||||
} else {
|
||||
document.documentElement.removeAttribute('data-theme')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Composable for managing the application theme (light/dark/system).
|
||||
* Extracted from TheSiteHeader. Listens for system preference changes
|
||||
* when set to "system" mode.
|
||||
*/
|
||||
export function useTheme(): UseThemeReturn {
|
||||
/**
|
||||
* Set and persist the current theme.
|
||||
*
|
||||
* @param theme - The theme to set
|
||||
*/
|
||||
function setTheme(theme: Theme): void {
|
||||
currentTheme.value = theme
|
||||
ls.set(LS_KEYS.THEME, theme)
|
||||
applyThemeToDocument(theme)
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the given or current theme to the document.
|
||||
*
|
||||
* @param theme - Optional theme override; uses currentTheme if not provided
|
||||
*/
|
||||
function applyTheme(theme?: Theme): void {
|
||||
applyThemeToDocument(theme ?? currentTheme.value)
|
||||
}
|
||||
|
||||
function handleSystemThemeChange(): void {
|
||||
if (currentTheme.value === THEME.SYSTEM) {
|
||||
applyThemeToDocument(THEME.SYSTEM)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
applyThemeToDocument(currentTheme.value)
|
||||
|
||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
|
||||
mediaQuery.addEventListener('change', handleSystemThemeChange)
|
||||
|
||||
mediaQueryCleanup = () => {
|
||||
mediaQuery.removeEventListener('change', handleSystemThemeChange)
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (mediaQueryCleanup) {
|
||||
mediaQueryCleanup()
|
||||
mediaQueryCleanup = null
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
currentTheme,
|
||||
setTheme,
|
||||
applyTheme,
|
||||
}
|
||||
}
|
||||
82
resources/scripts-v2/config/abilities.ts
Normal file
82
resources/scripts-v2/config/abilities.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
export const ABILITIES = {
|
||||
// Dashboard
|
||||
DASHBOARD: 'dashboard',
|
||||
|
||||
// Customers
|
||||
CREATE_CUSTOMER: 'create-customer',
|
||||
DELETE_CUSTOMER: 'delete-customer',
|
||||
EDIT_CUSTOMER: 'edit-customer',
|
||||
VIEW_CUSTOMER: 'view-customer',
|
||||
|
||||
// Items
|
||||
CREATE_ITEM: 'create-item',
|
||||
DELETE_ITEM: 'delete-item',
|
||||
EDIT_ITEM: 'edit-item',
|
||||
VIEW_ITEM: 'view-item',
|
||||
|
||||
// Tax Types
|
||||
CREATE_TAX_TYPE: 'create-tax-type',
|
||||
DELETE_TAX_TYPE: 'delete-tax-type',
|
||||
EDIT_TAX_TYPE: 'edit-tax-type',
|
||||
VIEW_TAX_TYPE: 'view-tax-type',
|
||||
|
||||
// Estimates
|
||||
CREATE_ESTIMATE: 'create-estimate',
|
||||
DELETE_ESTIMATE: 'delete-estimate',
|
||||
EDIT_ESTIMATE: 'edit-estimate',
|
||||
VIEW_ESTIMATE: 'view-estimate',
|
||||
SEND_ESTIMATE: 'send-estimate',
|
||||
|
||||
// Invoices
|
||||
CREATE_INVOICE: 'create-invoice',
|
||||
DELETE_INVOICE: 'delete-invoice',
|
||||
EDIT_INVOICE: 'edit-invoice',
|
||||
VIEW_INVOICE: 'view-invoice',
|
||||
SEND_INVOICE: 'send-invoice',
|
||||
|
||||
// Recurring Invoices
|
||||
CREATE_RECURRING_INVOICE: 'create-recurring-invoice',
|
||||
DELETE_RECURRING_INVOICE: 'delete-recurring-invoice',
|
||||
EDIT_RECURRING_INVOICE: 'edit-recurring-invoice',
|
||||
VIEW_RECURRING_INVOICE: 'view-recurring-invoice',
|
||||
|
||||
// Payments
|
||||
CREATE_PAYMENT: 'create-payment',
|
||||
DELETE_PAYMENT: 'delete-payment',
|
||||
EDIT_PAYMENT: 'edit-payment',
|
||||
VIEW_PAYMENT: 'view-payment',
|
||||
SEND_PAYMENT: 'send-payment',
|
||||
|
||||
// Expenses
|
||||
CREATE_EXPENSE: 'create-expense',
|
||||
DELETE_EXPENSE: 'delete-expense',
|
||||
EDIT_EXPENSE: 'edit-expense',
|
||||
VIEW_EXPENSE: 'view-expense',
|
||||
|
||||
// Custom Fields
|
||||
CREATE_CUSTOM_FIELDS: 'create-custom-field',
|
||||
DELETE_CUSTOM_FIELDS: 'delete-custom-field',
|
||||
EDIT_CUSTOM_FIELDS: 'edit-custom-field',
|
||||
VIEW_CUSTOM_FIELDS: 'view-custom-field',
|
||||
|
||||
// Roles
|
||||
CREATE_ROLE: 'create-role',
|
||||
DELETE_ROLE: 'delete-role',
|
||||
EDIT_ROLE: 'edit-role',
|
||||
VIEW_ROLE: 'view-role',
|
||||
|
||||
// Exchange Rates
|
||||
VIEW_EXCHANGE_RATE: 'view-exchange-rate-provider',
|
||||
CREATE_EXCHANGE_RATE: 'create-exchange-rate-provider',
|
||||
EDIT_EXCHANGE_RATE: 'edit-exchange-rate-provider',
|
||||
DELETE_EXCHANGE_RATE: 'delete-exchange-rate-provider',
|
||||
|
||||
// Reports
|
||||
VIEW_FINANCIAL_REPORT: 'view-financial-reports',
|
||||
|
||||
// Notes
|
||||
MANAGE_NOTE: 'manage-all-notes',
|
||||
VIEW_NOTE: 'view-all-notes',
|
||||
} as const
|
||||
|
||||
export type Ability = typeof ABILITIES[keyof typeof ABILITIES]
|
||||
101
resources/scripts-v2/config/constants.ts
Normal file
101
resources/scripts-v2/config/constants.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* App-wide constants for the InvoiceShelf application.
|
||||
*/
|
||||
|
||||
/** Document status values */
|
||||
export const DOCUMENT_STATUS = {
|
||||
DRAFT: 'DRAFT',
|
||||
SENT: 'SENT',
|
||||
VIEWED: 'VIEWED',
|
||||
EXPIRED: 'EXPIRED',
|
||||
ACCEPTED: 'ACCEPTED',
|
||||
REJECTED: 'REJECTED',
|
||||
PAID: 'PAID',
|
||||
UNPAID: 'UNPAID',
|
||||
PARTIALLY_PAID: 'PARTIALLY PAID',
|
||||
COMPLETED: 'COMPLETED',
|
||||
DUE: 'DUE',
|
||||
} as const
|
||||
|
||||
export type DocumentStatus = typeof DOCUMENT_STATUS[keyof typeof DOCUMENT_STATUS]
|
||||
|
||||
/** Badge color configuration for document statuses */
|
||||
export interface BadgeColor {
|
||||
bgColor: string
|
||||
color: string
|
||||
}
|
||||
|
||||
export const STATUS_BADGE_COLORS: Record<string, BadgeColor> = {
|
||||
DRAFT: { bgColor: '#F8EDCB', color: '#744210' },
|
||||
PAID: { bgColor: '#D5EED0', color: '#276749' },
|
||||
UNPAID: { bgColor: '#F8EDC', color: '#744210' },
|
||||
SENT: { bgColor: 'rgba(246, 208, 154, 0.4)', color: '#975a16' },
|
||||
REJECTED: { bgColor: '#E1E0EA', color: '#1A1841' },
|
||||
ACCEPTED: { bgColor: '#D5EED0', color: '#276749' },
|
||||
VIEWED: { bgColor: '#C9E3EC', color: '#2c5282' },
|
||||
EXPIRED: { bgColor: '#FED7D7', color: '#c53030' },
|
||||
'PARTIALLY PAID': { bgColor: '#C9E3EC', color: '#2c5282' },
|
||||
COMPLETED: { bgColor: '#D5EED0', color: '#276749' },
|
||||
DUE: { bgColor: '#F8EDCB', color: '#744210' },
|
||||
YES: { bgColor: '#D5EED0', color: '#276749' },
|
||||
NO: { bgColor: '#FED7D7', color: '#c53030' },
|
||||
}
|
||||
|
||||
/** Theme options */
|
||||
export const THEME = {
|
||||
LIGHT: 'light',
|
||||
DARK: 'dark',
|
||||
SYSTEM: 'system',
|
||||
} as const
|
||||
|
||||
export type Theme = typeof THEME[keyof typeof THEME]
|
||||
|
||||
/** Local storage keys used throughout the app */
|
||||
export const LS_KEYS = {
|
||||
AUTH_TOKEN: 'auth.token',
|
||||
SELECTED_COMPANY: 'selectedCompany',
|
||||
IS_ADMIN_MODE: 'isAdminMode',
|
||||
SIDEBAR_COLLAPSED: 'sidebarCollapsed',
|
||||
THEME: 'theme',
|
||||
} as const
|
||||
|
||||
/** Notification types */
|
||||
export const NOTIFICATION_TYPE = {
|
||||
SUCCESS: 'success',
|
||||
ERROR: 'error',
|
||||
INFO: 'info',
|
||||
WARNING: 'warning',
|
||||
} as const
|
||||
|
||||
export type NotificationType = typeof NOTIFICATION_TYPE[keyof typeof NOTIFICATION_TYPE]
|
||||
|
||||
/** Pagination defaults */
|
||||
export const PAGINATION_DEFAULTS = {
|
||||
PAGE: 1,
|
||||
LIMIT: 15,
|
||||
} as const
|
||||
|
||||
/** Dialog variant options */
|
||||
export const DIALOG_VARIANT = {
|
||||
PRIMARY: 'primary',
|
||||
DANGER: 'danger',
|
||||
} as const
|
||||
|
||||
export type DialogVariant = typeof DIALOG_VARIANT[keyof typeof DIALOG_VARIANT]
|
||||
|
||||
/** Modal size options */
|
||||
export const MODAL_SIZE = {
|
||||
SM: 'sm',
|
||||
MD: 'md',
|
||||
LG: 'lg',
|
||||
XL: 'xl',
|
||||
} as const
|
||||
|
||||
export type ModalSize = typeof MODAL_SIZE[keyof typeof MODAL_SIZE]
|
||||
|
||||
/** Valid image MIME types for uploads */
|
||||
export const VALID_IMAGE_TYPES = [
|
||||
'image/gif',
|
||||
'image/jpeg',
|
||||
'image/png',
|
||||
] as const
|
||||
23
resources/scripts-v2/config/index.ts
Normal file
23
resources/scripts-v2/config/index.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
export { ABILITIES } from './abilities'
|
||||
export type { Ability } from './abilities'
|
||||
|
||||
export {
|
||||
DOCUMENT_STATUS,
|
||||
STATUS_BADGE_COLORS,
|
||||
THEME,
|
||||
LS_KEYS,
|
||||
NOTIFICATION_TYPE,
|
||||
PAGINATION_DEFAULTS,
|
||||
DIALOG_VARIANT,
|
||||
MODAL_SIZE,
|
||||
VALID_IMAGE_TYPES,
|
||||
} from './constants'
|
||||
|
||||
export type {
|
||||
DocumentStatus,
|
||||
BadgeColor,
|
||||
Theme,
|
||||
NotificationType,
|
||||
DialogVariant,
|
||||
ModalSize,
|
||||
} from './constants'
|
||||
41
resources/scripts-v2/types/api.ts
Normal file
41
resources/scripts-v2/types/api.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
export interface ApiResponse<T> {
|
||||
data: T
|
||||
}
|
||||
|
||||
export interface PaginatedResponse<T> {
|
||||
data: T[]
|
||||
meta: PaginationMeta
|
||||
}
|
||||
|
||||
export interface PaginationMeta {
|
||||
current_page: number
|
||||
last_page: number
|
||||
per_page: number
|
||||
total: number
|
||||
}
|
||||
|
||||
export interface ApiError {
|
||||
message: string
|
||||
errors?: Record<string, string[]>
|
||||
}
|
||||
|
||||
export interface ListParams {
|
||||
page?: number
|
||||
limit?: number | 'all'
|
||||
orderByField?: string
|
||||
orderBy?: 'asc' | 'desc'
|
||||
search?: string
|
||||
}
|
||||
|
||||
export interface DateRangeParams {
|
||||
from_date?: string
|
||||
to_date?: string
|
||||
}
|
||||
|
||||
export interface NextNumberResponse {
|
||||
nextNumber: string
|
||||
}
|
||||
|
||||
export interface DeletePayload {
|
||||
ids: number[]
|
||||
}
|
||||
47
resources/scripts-v2/types/domain/company.ts
Normal file
47
resources/scripts-v2/types/domain/company.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import type { Address } from './user'
|
||||
import type { Role } from './role'
|
||||
import type { User } from './user'
|
||||
|
||||
export interface Company {
|
||||
id: number
|
||||
name: string
|
||||
vat_id: string | null
|
||||
tax_id: string | null
|
||||
logo: string | null
|
||||
logo_path: string | null
|
||||
unique_hash: string
|
||||
owner_id: number
|
||||
slug: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
address?: Address
|
||||
owner?: User
|
||||
roles: Role[]
|
||||
}
|
||||
|
||||
export interface CompanySetting {
|
||||
id: number
|
||||
company_id: number
|
||||
option: string
|
||||
value: string | null
|
||||
}
|
||||
|
||||
export interface CompanyInvitation {
|
||||
id: number
|
||||
company_id: number
|
||||
email: string
|
||||
token: string
|
||||
status: CompanyInvitationStatus
|
||||
expires_at: string
|
||||
created_at: string
|
||||
company?: Company
|
||||
role?: Role
|
||||
invited_by?: User
|
||||
}
|
||||
|
||||
export enum CompanyInvitationStatus {
|
||||
PENDING = 'pending',
|
||||
ACCEPTED = 'accepted',
|
||||
DECLINED = 'declined',
|
||||
EXPIRED = 'expired',
|
||||
}
|
||||
32
resources/scripts-v2/types/domain/currency.ts
Normal file
32
resources/scripts-v2/types/domain/currency.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
export interface Currency {
|
||||
id: number
|
||||
name: string
|
||||
code: string
|
||||
symbol: string
|
||||
precision: number
|
||||
thousand_separator: string
|
||||
decimal_separator: string
|
||||
swap_currency_symbol: boolean
|
||||
exchange_rate: number
|
||||
}
|
||||
|
||||
export interface ExchangeRateLog {
|
||||
id: number
|
||||
company_id: number
|
||||
base_currency_id: number
|
||||
currency_id: number
|
||||
exchange_rate: number
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export interface ExchangeRateProvider {
|
||||
id: number
|
||||
key: string
|
||||
driver: string
|
||||
currencies: string[]
|
||||
driver_config: Record<string, string>
|
||||
company_id: number
|
||||
active: boolean
|
||||
company?: import('./company').Company
|
||||
}
|
||||
62
resources/scripts-v2/types/domain/custom-field.ts
Normal file
62
resources/scripts-v2/types/domain/custom-field.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import type { Company } from './company'
|
||||
|
||||
export type CustomFieldType =
|
||||
| 'Text'
|
||||
| 'Textarea'
|
||||
| 'Phone'
|
||||
| 'URL'
|
||||
| 'Number'
|
||||
| 'Dropdown'
|
||||
| 'Switch'
|
||||
| 'Date'
|
||||
| 'Time'
|
||||
| 'DateTime'
|
||||
|
||||
export type CustomFieldModelType =
|
||||
| 'Customer'
|
||||
| 'Invoice'
|
||||
| 'Estimate'
|
||||
| 'Payment'
|
||||
| 'Expense'
|
||||
|
||||
export interface CustomField {
|
||||
id: number
|
||||
name: string
|
||||
slug: string
|
||||
label: string
|
||||
model_type: CustomFieldModelType
|
||||
type: CustomFieldType
|
||||
placeholder: string | null
|
||||
options: string[] | null
|
||||
boolean_answer: boolean | null
|
||||
date_answer: string | null
|
||||
time_answer: string | null
|
||||
string_answer: string | null
|
||||
number_answer: number | null
|
||||
date_time_answer: string | null
|
||||
is_required: boolean
|
||||
in_use: boolean
|
||||
order: number | null
|
||||
company_id: number
|
||||
default_answer: string | boolean | number | null
|
||||
company?: Company
|
||||
}
|
||||
|
||||
export interface CustomFieldValue {
|
||||
id: number
|
||||
custom_field_valuable_type: string
|
||||
custom_field_valuable_id: number
|
||||
type: CustomFieldType
|
||||
boolean_answer: boolean | null
|
||||
date_answer: string | null
|
||||
time_answer: string | null
|
||||
string_answer: string | null
|
||||
number_answer: number | null
|
||||
date_time_answer: string | null
|
||||
custom_field_id: number
|
||||
company_id: number
|
||||
default_answer: string | boolean | number | null
|
||||
default_formatted_answer: string | null
|
||||
custom_field?: CustomField
|
||||
company?: Company
|
||||
}
|
||||
57
resources/scripts-v2/types/domain/customer.ts
Normal file
57
resources/scripts-v2/types/domain/customer.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import type { Currency } from './currency'
|
||||
import type { Company } from './company'
|
||||
import type { Address } from './user'
|
||||
import type { CustomFieldValue } from './custom-field'
|
||||
|
||||
export interface Country {
|
||||
id: number
|
||||
code: string
|
||||
name: string
|
||||
phone_code: number
|
||||
}
|
||||
|
||||
export interface Customer {
|
||||
id: number
|
||||
name: string
|
||||
email: string | null
|
||||
phone: string | null
|
||||
contact_name: string | null
|
||||
company_name: string | null
|
||||
website: string | null
|
||||
enable_portal: boolean
|
||||
password_added: boolean
|
||||
currency_id: number | null
|
||||
company_id: number
|
||||
facebook_id: string | null
|
||||
google_id: string | null
|
||||
github_id: string | null
|
||||
created_at: string
|
||||
updated_at: string
|
||||
formatted_created_at: string
|
||||
avatar: string | number
|
||||
due_amount: number | null
|
||||
base_due_amount: number | null
|
||||
prefix: string | null
|
||||
tax_id: string | null
|
||||
billing?: Address
|
||||
shipping?: Address
|
||||
fields?: CustomFieldValue[]
|
||||
company?: Company
|
||||
currency?: Currency
|
||||
}
|
||||
|
||||
export interface CreateCustomerPayload {
|
||||
name: string
|
||||
contact_name?: string
|
||||
email?: string
|
||||
phone?: string | null
|
||||
password?: string
|
||||
confirm_password?: string
|
||||
currency_id: number | null
|
||||
website?: string | null
|
||||
billing?: Partial<Address>
|
||||
shipping?: Partial<Address>
|
||||
enable_portal?: boolean
|
||||
customFields?: CustomFieldValue[]
|
||||
fields?: CustomFieldValue[]
|
||||
}
|
||||
119
resources/scripts-v2/types/domain/estimate.ts
Normal file
119
resources/scripts-v2/types/domain/estimate.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import type { Customer } from './customer'
|
||||
import type { User } from './user'
|
||||
import type { Company } from './company'
|
||||
import type { Currency } from './currency'
|
||||
import type { Tax } from './tax'
|
||||
import type { CustomFieldValue } from './custom-field'
|
||||
import type { DiscountType } from './invoice'
|
||||
|
||||
export enum EstimateStatus {
|
||||
DRAFT = 'DRAFT',
|
||||
SENT = 'SENT',
|
||||
VIEWED = 'VIEWED',
|
||||
EXPIRED = 'EXPIRED',
|
||||
ACCEPTED = 'ACCEPTED',
|
||||
REJECTED = 'REJECTED',
|
||||
}
|
||||
|
||||
export interface EstimateItem {
|
||||
id: number | string
|
||||
name: string
|
||||
description: string | null
|
||||
discount_type: DiscountType
|
||||
quantity: number
|
||||
unit_name: string | null
|
||||
discount: number
|
||||
discount_val: number
|
||||
price: number
|
||||
tax: number
|
||||
total: number
|
||||
item_id: number | null
|
||||
estimate_id: number | null
|
||||
company_id: number
|
||||
exchange_rate: number
|
||||
base_discount_val: number
|
||||
base_price: number
|
||||
base_tax: number
|
||||
base_total: number
|
||||
taxes?: Tax[]
|
||||
fields?: CustomFieldValue[]
|
||||
}
|
||||
|
||||
export interface Estimate {
|
||||
id: number
|
||||
estimate_date: string
|
||||
expiry_date: string
|
||||
estimate_number: string
|
||||
status: EstimateStatus
|
||||
reference_number: string | null
|
||||
tax_per_item: string | null
|
||||
tax_included: boolean | null
|
||||
discount_per_item: string | null
|
||||
notes: string | null
|
||||
discount: number
|
||||
discount_type: DiscountType
|
||||
discount_val: number
|
||||
sub_total: number
|
||||
total: number
|
||||
tax: number
|
||||
unique_hash: string
|
||||
creator_id: number
|
||||
template_name: string | null
|
||||
customer_id: number
|
||||
exchange_rate: number
|
||||
base_discount_val: number
|
||||
base_sub_total: number
|
||||
base_total: number
|
||||
base_tax: number
|
||||
sequence_number: number
|
||||
currency_id: number
|
||||
formatted_expiry_date: string
|
||||
formatted_estimate_date: string
|
||||
estimate_pdf_url: string
|
||||
sales_tax_type: string | null
|
||||
sales_tax_address_type: string | null
|
||||
items?: EstimateItem[]
|
||||
customer?: Customer
|
||||
creator?: User
|
||||
taxes?: Tax[]
|
||||
fields?: CustomFieldValue[]
|
||||
company?: Company
|
||||
currency?: Currency
|
||||
}
|
||||
|
||||
export interface CreateEstimatePayload {
|
||||
estimate_date: string
|
||||
expiry_date: string
|
||||
estimate_number: string
|
||||
reference_number?: string | null
|
||||
customer_id: number
|
||||
template_name?: string | null
|
||||
notes?: string | null
|
||||
discount_type?: DiscountType
|
||||
discount?: number
|
||||
discount_val?: number
|
||||
tax_per_item?: string | null
|
||||
tax_included?: boolean | null
|
||||
discount_per_item?: string | null
|
||||
sales_tax_type?: string | null
|
||||
sales_tax_address_type?: string | null
|
||||
items: CreateEstimateItemPayload[]
|
||||
taxes?: Partial<Tax>[]
|
||||
customFields?: CustomFieldValue[]
|
||||
fields?: CustomFieldValue[]
|
||||
}
|
||||
|
||||
export interface CreateEstimateItemPayload {
|
||||
item_id?: number | null
|
||||
name: string
|
||||
description?: string | null
|
||||
quantity: number
|
||||
price: number
|
||||
discount_type?: DiscountType
|
||||
discount?: number
|
||||
discount_val?: number
|
||||
tax?: number
|
||||
total?: number
|
||||
taxes?: Partial<Tax>[]
|
||||
unit_name?: string | null
|
||||
}
|
||||
72
resources/scripts-v2/types/domain/expense.ts
Normal file
72
resources/scripts-v2/types/domain/expense.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import type { Customer } from './customer'
|
||||
import type { User } from './user'
|
||||
import type { Company } from './company'
|
||||
import type { Currency } from './currency'
|
||||
import type { PaymentMethod } from './payment'
|
||||
import type { CustomFieldValue } from './custom-field'
|
||||
|
||||
export interface ExpenseCategory {
|
||||
id: number
|
||||
name: string
|
||||
description: string | null
|
||||
company_id: number
|
||||
amount: number | null
|
||||
formatted_created_at: string
|
||||
company?: Company
|
||||
}
|
||||
|
||||
export interface ReceiptUrl {
|
||||
url: string
|
||||
type: string
|
||||
}
|
||||
|
||||
export interface ReceiptMeta {
|
||||
id: number
|
||||
name: string
|
||||
file_name: string
|
||||
mime_type: string
|
||||
size: number
|
||||
disk: string
|
||||
collection_name: string
|
||||
}
|
||||
|
||||
export interface Expense {
|
||||
id: number
|
||||
expense_date: string
|
||||
expense_number: string | null
|
||||
amount: number
|
||||
notes: string | null
|
||||
customer_id: number | null
|
||||
attachment_receipt_url: ReceiptUrl | null
|
||||
attachment_receipt: string | null
|
||||
attachment_receipt_meta: ReceiptMeta | null
|
||||
company_id: number
|
||||
expense_category_id: number | null
|
||||
creator_id: number
|
||||
formatted_expense_date: string
|
||||
formatted_created_at: string
|
||||
exchange_rate: number
|
||||
currency_id: number
|
||||
base_amount: number
|
||||
payment_method_id: number | null
|
||||
customer?: Customer
|
||||
expense_category?: ExpenseCategory
|
||||
creator?: User
|
||||
fields?: CustomFieldValue[]
|
||||
company?: Company
|
||||
currency?: Currency
|
||||
payment_method?: PaymentMethod
|
||||
}
|
||||
|
||||
export interface CreateExpensePayload {
|
||||
expense_date: string
|
||||
amount: number
|
||||
expense_category_id?: number | null
|
||||
customer_id?: number | null
|
||||
payment_method_id?: number | null
|
||||
notes?: string | null
|
||||
exchange_rate?: number
|
||||
currency_id?: number
|
||||
customFields?: CustomFieldValue[]
|
||||
fields?: CustomFieldValue[]
|
||||
}
|
||||
79
resources/scripts-v2/types/domain/index.ts
Normal file
79
resources/scripts-v2/types/domain/index.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
export type { Currency, ExchangeRateLog, ExchangeRateProvider } from './currency'
|
||||
|
||||
export type { Role, Ability } from './role'
|
||||
|
||||
export type { Company, CompanySetting, CompanyInvitation } from './company'
|
||||
export { CompanyInvitationStatus } from './company'
|
||||
|
||||
export type { Address, User, UserSetting } from './user'
|
||||
export { AddressType } from './user'
|
||||
|
||||
export type { Country, Customer, CreateCustomerPayload } from './customer'
|
||||
|
||||
export type {
|
||||
Invoice,
|
||||
InvoiceItem,
|
||||
CreateInvoicePayload,
|
||||
CreateInvoiceItemPayload,
|
||||
DiscountType,
|
||||
} from './invoice'
|
||||
export { InvoiceStatus, InvoicePaidStatus } from './invoice'
|
||||
|
||||
export type {
|
||||
Estimate,
|
||||
EstimateItem,
|
||||
CreateEstimatePayload,
|
||||
CreateEstimateItemPayload,
|
||||
} from './estimate'
|
||||
export { EstimateStatus } from './estimate'
|
||||
|
||||
export type {
|
||||
RecurringInvoice,
|
||||
CreateRecurringInvoicePayload,
|
||||
} from './recurring-invoice'
|
||||
export {
|
||||
RecurringInvoiceStatus,
|
||||
RecurringInvoiceLimitBy,
|
||||
} from './recurring-invoice'
|
||||
|
||||
export type {
|
||||
Payment,
|
||||
PaymentMethod,
|
||||
Transaction,
|
||||
CreatePaymentPayload,
|
||||
} from './payment'
|
||||
|
||||
export type {
|
||||
Expense,
|
||||
ExpenseCategory,
|
||||
ReceiptUrl,
|
||||
ReceiptMeta,
|
||||
CreateExpensePayload,
|
||||
} from './expense'
|
||||
|
||||
export type { Item, Unit } from './item'
|
||||
|
||||
export type { TaxType, Tax } from './tax'
|
||||
export { TaxTypeCategory } from './tax'
|
||||
|
||||
export type {
|
||||
CustomField,
|
||||
CustomFieldValue,
|
||||
CustomFieldType,
|
||||
CustomFieldModelType,
|
||||
} from './custom-field'
|
||||
|
||||
export type { Note, NoteType } from './note'
|
||||
|
||||
export type {
|
||||
Module,
|
||||
InstalledModule,
|
||||
ModuleAuthor,
|
||||
ModuleVersion,
|
||||
ModuleLink,
|
||||
ModuleReview,
|
||||
ModuleScreenshot,
|
||||
ModuleFaq,
|
||||
} from './module'
|
||||
|
||||
export type { Setting, CompanySettingsMap } from './setting'
|
||||
135
resources/scripts-v2/types/domain/invoice.ts
Normal file
135
resources/scripts-v2/types/domain/invoice.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
import type { Customer } from './customer'
|
||||
import type { User } from './user'
|
||||
import type { Company } from './company'
|
||||
import type { Currency } from './currency'
|
||||
import type { Tax } from './tax'
|
||||
import type { CustomFieldValue } from './custom-field'
|
||||
|
||||
export enum InvoiceStatus {
|
||||
DRAFT = 'DRAFT',
|
||||
SENT = 'SENT',
|
||||
VIEWED = 'VIEWED',
|
||||
COMPLETED = 'COMPLETED',
|
||||
}
|
||||
|
||||
export enum InvoicePaidStatus {
|
||||
UNPAID = 'UNPAID',
|
||||
PARTIALLY_PAID = 'PARTIALLY_PAID',
|
||||
PAID = 'PAID',
|
||||
}
|
||||
|
||||
export type DiscountType = 'fixed' | 'percentage'
|
||||
|
||||
export interface InvoiceItem {
|
||||
id: number | string
|
||||
name: string
|
||||
description: string | null
|
||||
discount_type: DiscountType
|
||||
price: number
|
||||
quantity: number
|
||||
unit_name: string | null
|
||||
discount: number
|
||||
discount_val: number
|
||||
tax: number
|
||||
total: number
|
||||
invoice_id: number | null
|
||||
item_id: number | null
|
||||
company_id: number
|
||||
base_price: number
|
||||
exchange_rate: number
|
||||
base_discount_val: number
|
||||
base_tax: number
|
||||
base_total: number
|
||||
recurring_invoice_id: number | null
|
||||
taxes?: Tax[]
|
||||
fields?: CustomFieldValue[]
|
||||
}
|
||||
|
||||
export interface Invoice {
|
||||
id: number
|
||||
invoice_date: string
|
||||
due_date: string
|
||||
invoice_number: string
|
||||
reference_number: string | null
|
||||
status: InvoiceStatus
|
||||
paid_status: InvoicePaidStatus
|
||||
tax_per_item: string | null
|
||||
tax_included: boolean | null
|
||||
discount_per_item: string | null
|
||||
notes: string | null
|
||||
discount_type: DiscountType
|
||||
discount: number
|
||||
discount_val: number
|
||||
sub_total: number
|
||||
total: number
|
||||
tax: number
|
||||
due_amount: number
|
||||
sent: boolean | null
|
||||
viewed: boolean | null
|
||||
unique_hash: string
|
||||
template_name: string | null
|
||||
customer_id: number
|
||||
recurring_invoice_id: number | null
|
||||
sequence_number: number
|
||||
exchange_rate: number
|
||||
base_discount_val: number
|
||||
base_sub_total: number
|
||||
base_total: number
|
||||
creator_id: number
|
||||
base_tax: number
|
||||
base_due_amount: number
|
||||
currency_id: number
|
||||
formatted_created_at: string
|
||||
invoice_pdf_url: string
|
||||
formatted_invoice_date: string
|
||||
formatted_due_date: string
|
||||
allow_edit: boolean
|
||||
payment_module_enabled: boolean
|
||||
sales_tax_type: string | null
|
||||
sales_tax_address_type: string | null
|
||||
overdue: boolean | null
|
||||
items?: InvoiceItem[]
|
||||
customer?: Customer
|
||||
creator?: User
|
||||
taxes?: Tax[]
|
||||
fields?: CustomFieldValue[]
|
||||
company?: Company
|
||||
currency?: Currency
|
||||
}
|
||||
|
||||
export interface CreateInvoicePayload {
|
||||
invoice_date: string
|
||||
due_date: string
|
||||
invoice_number: string
|
||||
reference_number?: string | null
|
||||
customer_id: number
|
||||
template_name?: string | null
|
||||
notes?: string | null
|
||||
discount_type?: DiscountType
|
||||
discount?: number
|
||||
discount_val?: number
|
||||
tax_per_item?: string | null
|
||||
tax_included?: boolean | null
|
||||
discount_per_item?: string | null
|
||||
sales_tax_type?: string | null
|
||||
sales_tax_address_type?: string | null
|
||||
items: CreateInvoiceItemPayload[]
|
||||
taxes?: Partial<Tax>[]
|
||||
customFields?: CustomFieldValue[]
|
||||
fields?: CustomFieldValue[]
|
||||
}
|
||||
|
||||
export interface CreateInvoiceItemPayload {
|
||||
item_id?: number | null
|
||||
name: string
|
||||
description?: string | null
|
||||
quantity: number
|
||||
price: number
|
||||
discount_type?: DiscountType
|
||||
discount?: number
|
||||
discount_val?: number
|
||||
tax?: number
|
||||
total?: number
|
||||
taxes?: Partial<Tax>[]
|
||||
unit_name?: string | null
|
||||
}
|
||||
29
resources/scripts-v2/types/domain/item.ts
Normal file
29
resources/scripts-v2/types/domain/item.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import type { Company } from './company'
|
||||
import type { Currency } from './currency'
|
||||
import type { Tax } from './tax'
|
||||
|
||||
export interface Unit {
|
||||
id: number
|
||||
name: string
|
||||
company_id: number
|
||||
company?: Company
|
||||
}
|
||||
|
||||
export interface Item {
|
||||
id: number
|
||||
name: string
|
||||
description: string | null
|
||||
price: number
|
||||
unit_id: number | null
|
||||
company_id: number
|
||||
creator_id: number
|
||||
currency_id: number | null
|
||||
created_at: string
|
||||
updated_at: string
|
||||
tax_per_item: string | null
|
||||
formatted_created_at: string
|
||||
unit?: Unit
|
||||
company?: Company
|
||||
taxes?: Tax[]
|
||||
currency?: Currency
|
||||
}
|
||||
76
resources/scripts-v2/types/domain/module.ts
Normal file
76
resources/scripts-v2/types/domain/module.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
export interface ModuleAuthor {
|
||||
name: string
|
||||
avatar: string
|
||||
}
|
||||
|
||||
export interface ModuleVersion {
|
||||
module_version: string
|
||||
invoiceshelf_version: string
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export interface ModuleLink {
|
||||
name: string
|
||||
url: string
|
||||
}
|
||||
|
||||
export interface ModuleReview {
|
||||
id: number
|
||||
rating: number
|
||||
comment: string
|
||||
user: string
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export interface ModuleScreenshot {
|
||||
url: string
|
||||
title: string | null
|
||||
}
|
||||
|
||||
export interface ModuleFaq {
|
||||
question: string
|
||||
answer: string
|
||||
}
|
||||
|
||||
export interface Module {
|
||||
id: number
|
||||
average_rating: number | null
|
||||
cover: string | null
|
||||
slug: string
|
||||
module_name: string
|
||||
faq: ModuleFaq[] | null
|
||||
highlights: string[] | null
|
||||
installed_module_version: string | null
|
||||
installed_module_version_updated_at: string | null
|
||||
latest_module_version: string
|
||||
latest_module_version_updated_at: string
|
||||
is_dev: boolean
|
||||
license: string | null
|
||||
long_description: string | null
|
||||
monthly_price: number | null
|
||||
name: string
|
||||
purchased: boolean
|
||||
reviews: ModuleReview[]
|
||||
screenshots: ModuleScreenshot[] | null
|
||||
short_description: string | null
|
||||
type: string | null
|
||||
yearly_price: number | null
|
||||
author_name: string
|
||||
author_avatar: string
|
||||
installed: boolean
|
||||
enabled: boolean
|
||||
update_available: boolean
|
||||
video_link: string | null
|
||||
video_thumbnail: string | null
|
||||
links: ModuleLink[] | null
|
||||
}
|
||||
|
||||
export interface InstalledModule {
|
||||
id: number
|
||||
name: string
|
||||
version: string
|
||||
installed: boolean
|
||||
enabled: boolean
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
12
resources/scripts-v2/types/domain/note.ts
Normal file
12
resources/scripts-v2/types/domain/note.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import type { Company } from './company'
|
||||
|
||||
export type NoteType = 'Invoice' | 'Estimate' | 'Payment'
|
||||
|
||||
export interface Note {
|
||||
id: number
|
||||
type: NoteType
|
||||
name: string
|
||||
notes: string
|
||||
is_default: boolean | null
|
||||
company?: Company
|
||||
}
|
||||
67
resources/scripts-v2/types/domain/payment.ts
Normal file
67
resources/scripts-v2/types/domain/payment.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import type { Customer } from './customer'
|
||||
import type { Invoice } from './invoice'
|
||||
import type { Company } from './company'
|
||||
import type { Currency } from './currency'
|
||||
import type { User } from './user'
|
||||
import type { CustomFieldValue } from './custom-field'
|
||||
|
||||
export interface PaymentMethod {
|
||||
id: number
|
||||
name: string
|
||||
company_id: number
|
||||
type: string | null
|
||||
company?: Company
|
||||
}
|
||||
|
||||
export interface Transaction {
|
||||
id: number
|
||||
transaction_id: string
|
||||
type: string
|
||||
status: string
|
||||
transaction_date: string
|
||||
invoice_id: number | null
|
||||
invoice?: Invoice
|
||||
company?: Company
|
||||
}
|
||||
|
||||
export interface Payment {
|
||||
id: number
|
||||
payment_number: string
|
||||
payment_date: string
|
||||
notes: string | null
|
||||
amount: number
|
||||
unique_hash: string
|
||||
invoice_id: number | null
|
||||
company_id: number
|
||||
payment_method_id: number | null
|
||||
creator_id: number
|
||||
customer_id: number
|
||||
exchange_rate: number
|
||||
base_amount: number
|
||||
currency_id: number
|
||||
transaction_id: number | null
|
||||
sequence_number: number
|
||||
formatted_created_at: string
|
||||
formatted_payment_date: string
|
||||
payment_pdf_url: string
|
||||
customer?: Customer
|
||||
invoice?: Invoice
|
||||
payment_method?: PaymentMethod
|
||||
fields?: CustomFieldValue[]
|
||||
company?: Company
|
||||
currency?: Currency
|
||||
transaction?: Transaction
|
||||
}
|
||||
|
||||
export interface CreatePaymentPayload {
|
||||
payment_date: string
|
||||
payment_number: string
|
||||
customer_id: number
|
||||
amount: number
|
||||
invoice_id?: number | null
|
||||
payment_method_id?: number | null
|
||||
notes?: string | null
|
||||
exchange_rate?: number
|
||||
customFields?: CustomFieldValue[]
|
||||
fields?: CustomFieldValue[]
|
||||
}
|
||||
86
resources/scripts-v2/types/domain/recurring-invoice.ts
Normal file
86
resources/scripts-v2/types/domain/recurring-invoice.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import type { Customer } from './customer'
|
||||
import type { User } from './user'
|
||||
import type { Company } from './company'
|
||||
import type { Currency } from './currency'
|
||||
import type { Tax } from './tax'
|
||||
import type { Invoice, InvoiceItem } from './invoice'
|
||||
import type { CustomFieldValue } from './custom-field'
|
||||
import type { DiscountType } from './invoice'
|
||||
|
||||
export enum RecurringInvoiceStatus {
|
||||
ACTIVE = 'ACTIVE',
|
||||
ON_HOLD = 'ON_HOLD',
|
||||
COMPLETED = 'COMPLETED',
|
||||
}
|
||||
|
||||
export enum RecurringInvoiceLimitBy {
|
||||
NONE = 'NONE',
|
||||
COUNT = 'COUNT',
|
||||
DATE = 'DATE',
|
||||
}
|
||||
|
||||
export interface RecurringInvoice {
|
||||
id: number
|
||||
starts_at: string
|
||||
formatted_starts_at: string
|
||||
formatted_created_at: string
|
||||
formatted_next_invoice_at: string
|
||||
formatted_limit_date: string
|
||||
send_automatically: boolean
|
||||
customer_id: number
|
||||
company_id: number
|
||||
creator_id: number
|
||||
status: RecurringInvoiceStatus
|
||||
next_invoice_at: string
|
||||
frequency: string
|
||||
limit_by: RecurringInvoiceLimitBy
|
||||
limit_count: number | null
|
||||
limit_date: string | null
|
||||
exchange_rate: number
|
||||
tax_per_item: string | null
|
||||
tax_included: boolean | null
|
||||
discount_per_item: string | null
|
||||
notes: string | null
|
||||
discount_type: DiscountType
|
||||
discount: number
|
||||
discount_val: number
|
||||
sub_total: number
|
||||
total: number
|
||||
tax: number
|
||||
due_amount: number
|
||||
template_name: string | null
|
||||
sales_tax_type: string | null
|
||||
sales_tax_address_type: string | null
|
||||
fields?: CustomFieldValue[]
|
||||
items?: InvoiceItem[]
|
||||
customer?: Customer
|
||||
company?: Company
|
||||
invoices?: Invoice[]
|
||||
taxes?: Tax[]
|
||||
creator?: User
|
||||
currency?: Currency
|
||||
}
|
||||
|
||||
export interface CreateRecurringInvoicePayload {
|
||||
starts_at: string
|
||||
frequency: string
|
||||
customer_id: number
|
||||
send_automatically?: boolean
|
||||
limit_by?: RecurringInvoiceLimitBy
|
||||
limit_count?: number | null
|
||||
limit_date?: string | null
|
||||
template_name?: string | null
|
||||
notes?: string | null
|
||||
discount_type?: DiscountType
|
||||
discount?: number
|
||||
discount_val?: number
|
||||
tax_per_item?: string | null
|
||||
tax_included?: boolean | null
|
||||
discount_per_item?: string | null
|
||||
sales_tax_type?: string | null
|
||||
sales_tax_address_type?: string | null
|
||||
items: Partial<InvoiceItem>[]
|
||||
taxes?: Partial<Tax>[]
|
||||
customFields?: CustomFieldValue[]
|
||||
fields?: CustomFieldValue[]
|
||||
}
|
||||
20
resources/scripts-v2/types/domain/role.ts
Normal file
20
resources/scripts-v2/types/domain/role.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
export interface Ability {
|
||||
id: number
|
||||
name: string
|
||||
title: string | null
|
||||
entity_id: number | null
|
||||
entity_type: string | null
|
||||
only_owned: boolean
|
||||
scope: number | null
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export interface Role {
|
||||
id: number
|
||||
name: string
|
||||
title: string | null
|
||||
level: number | null
|
||||
formatted_created_at: string
|
||||
abilities: Ability[]
|
||||
}
|
||||
58
resources/scripts-v2/types/domain/setting.ts
Normal file
58
resources/scripts-v2/types/domain/setting.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* Global application setting (not company-scoped).
|
||||
* Corresponds to the `settings` table.
|
||||
*/
|
||||
export interface Setting {
|
||||
id: number
|
||||
option: string
|
||||
value: string | null
|
||||
}
|
||||
|
||||
/**
|
||||
* Common company settings that are frequently accessed on the frontend.
|
||||
* These correspond to key-value pairs in the company_settings table.
|
||||
* Use this typed map when reading settings from the store, rather than
|
||||
* accessing raw CompanySetting rows.
|
||||
*/
|
||||
export interface CompanySettingsMap {
|
||||
currency: string
|
||||
time_zone: string
|
||||
language: string
|
||||
fiscal_year: string
|
||||
carbon_date_format: string
|
||||
carbon_time_format: string
|
||||
moment_date_format: string
|
||||
notification_email: string
|
||||
tax_per_item: 'YES' | 'NO'
|
||||
discount_per_item: 'YES' | 'NO'
|
||||
invoice_prefix: string
|
||||
invoice_auto_generate: string
|
||||
estimate_prefix: string
|
||||
estimate_auto_generate: string
|
||||
payment_prefix: string
|
||||
payment_auto_generate: string
|
||||
invoice_mail_body: string
|
||||
estimate_mail_body: string
|
||||
payment_mail_body: string
|
||||
invoice_company_address_format: string
|
||||
invoice_shipping_address_format: string
|
||||
invoice_billing_address_format: string
|
||||
estimate_company_address_format: string
|
||||
estimate_shipping_address_format: string
|
||||
estimate_billing_address_format: string
|
||||
payment_company_address_format: string
|
||||
payment_from_customer_address_format: string
|
||||
invoice_email_attachment: 'YES' | 'NO'
|
||||
estimate_email_attachment: 'YES' | 'NO'
|
||||
payment_email_attachment: 'YES' | 'NO'
|
||||
retrospective_edits: string
|
||||
invoice_set_due_date_automatically: 'YES' | 'NO'
|
||||
invoice_due_date_days: string
|
||||
estimate_set_expiry_date_automatically: 'YES' | 'NO'
|
||||
estimate_expiry_date_days: string
|
||||
estimate_convert_action: string
|
||||
invoice_use_time: 'YES' | 'NO'
|
||||
sales_tax_type: string | null
|
||||
sales_tax_address_type: string | null
|
||||
[key: string]: string | null
|
||||
}
|
||||
44
resources/scripts-v2/types/domain/tax.ts
Normal file
44
resources/scripts-v2/types/domain/tax.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import type { Currency } from './currency'
|
||||
import type { Company } from './company'
|
||||
|
||||
export enum TaxTypeCategory {
|
||||
GENERAL = 'GENERAL',
|
||||
MODULE = 'MODULE',
|
||||
}
|
||||
|
||||
export interface TaxType {
|
||||
id: number
|
||||
name: string
|
||||
percent: number
|
||||
fixed_amount: number
|
||||
calculation_type: string | null
|
||||
type: TaxTypeCategory
|
||||
compound_tax: boolean
|
||||
collective_tax: number | null
|
||||
description: string | null
|
||||
company_id: number
|
||||
company?: Company
|
||||
}
|
||||
|
||||
export interface Tax {
|
||||
id: number
|
||||
tax_type_id: number
|
||||
invoice_id: number | null
|
||||
estimate_id: number | null
|
||||
invoice_item_id: number | null
|
||||
estimate_item_id: number | null
|
||||
item_id: number | null
|
||||
company_id: number
|
||||
name: string
|
||||
amount: number
|
||||
percent: number
|
||||
calculation_type: string | null
|
||||
fixed_amount: number
|
||||
compound_tax: boolean
|
||||
base_amount: number
|
||||
currency_id: number | null
|
||||
type: TaxTypeCategory
|
||||
recurring_invoice_id: number | null
|
||||
tax_type?: TaxType
|
||||
currency?: Currency
|
||||
}
|
||||
60
resources/scripts-v2/types/domain/user.ts
Normal file
60
resources/scripts-v2/types/domain/user.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import type { Currency } from './currency'
|
||||
import type { Company } from './company'
|
||||
import type { Role } from './role'
|
||||
import type { Country } from './customer'
|
||||
|
||||
export interface Address {
|
||||
id: number
|
||||
name: string | null
|
||||
address_street_1: string | null
|
||||
address_street_2: string | null
|
||||
city: string | null
|
||||
state: string | null
|
||||
country_id: number | null
|
||||
zip: string | null
|
||||
phone: string | null
|
||||
fax: string | null
|
||||
type: AddressType
|
||||
user_id: number | null
|
||||
company_id: number | null
|
||||
customer_id: number | null
|
||||
country?: Country
|
||||
user?: User
|
||||
}
|
||||
|
||||
export enum AddressType {
|
||||
BILLING = 'billing',
|
||||
SHIPPING = 'shipping',
|
||||
}
|
||||
|
||||
export interface User {
|
||||
id: number
|
||||
name: string
|
||||
email: string
|
||||
phone: string | null
|
||||
role: string | null
|
||||
contact_name: string | null
|
||||
company_name: string | null
|
||||
website: string | null
|
||||
enable_portal: boolean | null
|
||||
currency_id: number | null
|
||||
facebook_id: string | null
|
||||
google_id: string | null
|
||||
github_id: string | null
|
||||
created_at: string
|
||||
updated_at: string
|
||||
avatar: string | number
|
||||
is_owner: boolean
|
||||
is_super_admin: boolean
|
||||
roles: Role[]
|
||||
formatted_created_at: string
|
||||
currency?: Currency
|
||||
companies?: Company[]
|
||||
}
|
||||
|
||||
export interface UserSetting {
|
||||
id: number
|
||||
key: string
|
||||
value: string | null
|
||||
user_id: number
|
||||
}
|
||||
9
resources/scripts-v2/types/env.d.ts
vendored
Normal file
9
resources/scripts-v2/types/env.d.ts
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_APP_TITLE: string
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv
|
||||
}
|
||||
78
resources/scripts-v2/types/index.ts
Normal file
78
resources/scripts-v2/types/index.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
export type {
|
||||
ApiResponse,
|
||||
PaginatedResponse,
|
||||
PaginationMeta,
|
||||
ApiError,
|
||||
ListParams,
|
||||
DateRangeParams,
|
||||
NextNumberResponse,
|
||||
DeletePayload,
|
||||
} from './api'
|
||||
|
||||
export type {
|
||||
Currency,
|
||||
ExchangeRateLog,
|
||||
ExchangeRateProvider,
|
||||
Role,
|
||||
Ability,
|
||||
Company,
|
||||
CompanySetting,
|
||||
CompanyInvitation,
|
||||
Address,
|
||||
User,
|
||||
UserSetting,
|
||||
Country,
|
||||
Customer,
|
||||
CreateCustomerPayload,
|
||||
Invoice,
|
||||
InvoiceItem,
|
||||
CreateInvoicePayload,
|
||||
CreateInvoiceItemPayload,
|
||||
DiscountType,
|
||||
Estimate,
|
||||
EstimateItem,
|
||||
CreateEstimatePayload,
|
||||
CreateEstimateItemPayload,
|
||||
RecurringInvoice,
|
||||
CreateRecurringInvoicePayload,
|
||||
Payment,
|
||||
PaymentMethod,
|
||||
Transaction,
|
||||
CreatePaymentPayload,
|
||||
Expense,
|
||||
ExpenseCategory,
|
||||
ReceiptUrl,
|
||||
ReceiptMeta,
|
||||
CreateExpensePayload,
|
||||
Item,
|
||||
Unit,
|
||||
TaxType,
|
||||
Tax,
|
||||
CustomField,
|
||||
CustomFieldValue,
|
||||
CustomFieldType,
|
||||
CustomFieldModelType,
|
||||
Note,
|
||||
NoteType,
|
||||
Module,
|
||||
InstalledModule,
|
||||
ModuleAuthor,
|
||||
ModuleVersion,
|
||||
ModuleLink,
|
||||
ModuleReview,
|
||||
ModuleScreenshot,
|
||||
ModuleFaq,
|
||||
Setting,
|
||||
CompanySettingsMap,
|
||||
} from './domain'
|
||||
|
||||
export {
|
||||
CompanyInvitationStatus,
|
||||
AddressType,
|
||||
InvoiceStatus,
|
||||
InvoicePaidStatus,
|
||||
EstimateStatus,
|
||||
RecurringInvoiceStatus,
|
||||
RecurringInvoiceLimitBy,
|
||||
TaxTypeCategory,
|
||||
} from './domain'
|
||||
9
resources/scripts-v2/types/vue-shims.d.ts
vendored
Normal file
9
resources/scripts-v2/types/vue-shims.d.ts
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
declare module '*.vue' {
|
||||
import type { DefineComponent } from 'vue'
|
||||
const component: DefineComponent<
|
||||
Record<string, unknown>,
|
||||
Record<string, unknown>,
|
||||
unknown
|
||||
>
|
||||
export default component
|
||||
}
|
||||
165
resources/scripts-v2/utils/error-handling.ts
Normal file
165
resources/scripts-v2/utils/error-handling.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
import type { ApiError } from '../types/api'
|
||||
|
||||
/**
|
||||
* Shape of an Axios-like error response.
|
||||
*/
|
||||
interface AxiosLikeError {
|
||||
response?: {
|
||||
status?: number
|
||||
statusText?: string
|
||||
data?: {
|
||||
message?: string
|
||||
error?: string | boolean
|
||||
errors?: Record<string, string[]>
|
||||
}
|
||||
}
|
||||
message?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalized API error result.
|
||||
*/
|
||||
export interface NormalizedApiError {
|
||||
message: string
|
||||
statusCode: number | null
|
||||
validationErrors: Record<string, string[]>
|
||||
isUnauthorized: boolean
|
||||
isValidationError: boolean
|
||||
isNetworkError: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Known error message to translation key map.
|
||||
*/
|
||||
const ERROR_TRANSLATION_MAP: Record<string, string> = {
|
||||
'These credentials do not match our records.': 'errors.login_invalid_credentials',
|
||||
'invalid_key': 'errors.invalid_provider_key',
|
||||
'This feature is available on Starter plan and onwards!': 'errors.starter_plan',
|
||||
'taxes_attached': 'settings.tax_types.already_in_use',
|
||||
'expense_attached': 'settings.expense_category.already_in_use',
|
||||
'payments_attached': 'settings.payment_modes.payments_attached',
|
||||
'expenses_attached': 'settings.payment_modes.expenses_attached',
|
||||
'role_attached_to_users': 'settings.roles.already_in_use',
|
||||
'items_attached': 'settings.customization.items.already_in_use',
|
||||
'payment_attached_message': 'invoices.payment_attached_message',
|
||||
'The email has already been taken.': 'validation.email_already_taken',
|
||||
'Relation estimateItems exists.': 'items.item_attached_message',
|
||||
'Relation invoiceItems exists.': 'items.item_attached_message',
|
||||
'Relation taxes exists.': 'settings.tax_types.already_in_use',
|
||||
'Relation payments exists.': 'errors.payment_attached',
|
||||
'The estimate number has already been taken.': 'errors.estimate_number_used',
|
||||
'The payment number has already been taken.': 'errors.estimate_number_used',
|
||||
'The invoice number has already been taken.': 'errors.invoice_number_used',
|
||||
'The name has already been taken.': 'errors.name_already_taken',
|
||||
'total_invoice_amount_must_be_more_than_paid_amount': 'invoices.invalid_due_amount_message',
|
||||
'you_cannot_edit_currency': 'customers.edit_currency_not_allowed',
|
||||
'receipt_does_not_exist': 'errors.receipt_does_not_exist',
|
||||
'customer_cannot_be_changed_after_payment_is_added': 'errors.customer_cannot_be_changed_after_payment_is_added',
|
||||
'invalid_credentials': 'errors.invalid_credentials',
|
||||
'not_allowed': 'errors.not_allowed',
|
||||
'invalid_state': 'errors.invalid_state',
|
||||
'invalid_city': 'errors.invalid_city',
|
||||
'invalid_postal_code': 'errors.invalid_postal_code',
|
||||
'invalid_format': 'errors.invalid_format',
|
||||
'api_error': 'errors.api_error',
|
||||
'feature_not_enabled': 'errors.feature_not_enabled',
|
||||
'request_limit_met': 'errors.request_limit_met',
|
||||
'address_incomplete': 'errors.address_incomplete',
|
||||
'invalid_address': 'errors.invalid_address',
|
||||
'Email could not be sent to this email address.': 'errors.email_could_not_be_sent',
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an API error and return a normalized error object.
|
||||
*
|
||||
* @param err - The error from an API call (typically an Axios error)
|
||||
* @returns A normalized error with extracted message, status, and validation errors
|
||||
*/
|
||||
export function handleApiError(err: unknown): NormalizedApiError {
|
||||
const axiosError = err as AxiosLikeError
|
||||
|
||||
if (!axiosError.response) {
|
||||
return {
|
||||
message: 'Please check your internet connection or wait until servers are back online.',
|
||||
statusCode: null,
|
||||
validationErrors: {},
|
||||
isUnauthorized: false,
|
||||
isValidationError: false,
|
||||
isNetworkError: true,
|
||||
}
|
||||
}
|
||||
|
||||
const { response } = axiosError
|
||||
const statusCode = response.status ?? null
|
||||
const isUnauthorized =
|
||||
response.statusText === 'Unauthorized' ||
|
||||
response.data?.message === ' Unauthorized.' ||
|
||||
statusCode === 401
|
||||
|
||||
if (isUnauthorized) {
|
||||
const message = response.data?.message ?? 'Unauthorized'
|
||||
return {
|
||||
message,
|
||||
statusCode,
|
||||
validationErrors: {},
|
||||
isUnauthorized: true,
|
||||
isValidationError: false,
|
||||
isNetworkError: false,
|
||||
}
|
||||
}
|
||||
|
||||
const validationErrors = response.data?.errors ?? {}
|
||||
const isValidationError = Object.keys(validationErrors).length > 0
|
||||
|
||||
if (isValidationError) {
|
||||
const firstErrorKey = Object.keys(validationErrors)[0]
|
||||
const firstErrorMessage = validationErrors[firstErrorKey]?.[0] ?? 'Validation error'
|
||||
return {
|
||||
message: firstErrorMessage,
|
||||
statusCode,
|
||||
validationErrors,
|
||||
isUnauthorized: false,
|
||||
isValidationError: true,
|
||||
isNetworkError: false,
|
||||
}
|
||||
}
|
||||
|
||||
const errorField = response.data?.error
|
||||
let message: string
|
||||
|
||||
if (typeof errorField === 'string') {
|
||||
message = errorField
|
||||
} else {
|
||||
message = response.data?.message ?? 'An unexpected error occurred'
|
||||
}
|
||||
|
||||
return {
|
||||
message,
|
||||
statusCode,
|
||||
validationErrors: {},
|
||||
isUnauthorized: false,
|
||||
isValidationError: false,
|
||||
isNetworkError: false,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract validation errors from an API error response.
|
||||
*
|
||||
* @param err - The error from an API call
|
||||
* @returns A record mapping field names to arrays of error messages
|
||||
*/
|
||||
export function extractValidationErrors(err: unknown): Record<string, string[]> {
|
||||
const axiosError = err as AxiosLikeError
|
||||
return axiosError.response?.data?.errors ?? {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Look up the translation key for a known error message.
|
||||
*
|
||||
* @param errorMessage - The raw error message
|
||||
* @returns The translation key if known, or null if not mapped
|
||||
*/
|
||||
export function getErrorTranslationKey(errorMessage: string): string | null {
|
||||
return ERROR_TRANSLATION_MAP[errorMessage] ?? null
|
||||
}
|
||||
105
resources/scripts-v2/utils/format-date.ts
Normal file
105
resources/scripts-v2/utils/format-date.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import { format, formatDistanceToNow, parseISO, isValid } from 'date-fns'
|
||||
import type { Locale } from 'date-fns'
|
||||
|
||||
/**
|
||||
* Default date format used across the application.
|
||||
*/
|
||||
export const DEFAULT_DATE_FORMAT = 'yyyy-MM-dd'
|
||||
|
||||
/**
|
||||
* Default datetime format used across the application.
|
||||
*/
|
||||
export const DEFAULT_DATETIME_FORMAT = 'yyyy-MM-dd HH:mm:ss'
|
||||
|
||||
/**
|
||||
* Format a date value into a string using the given format pattern.
|
||||
*
|
||||
* @param date - A Date object, ISO string, or timestamp
|
||||
* @param formatStr - A date-fns format pattern (default: 'yyyy-MM-dd')
|
||||
* @param options - Optional locale for localized formatting
|
||||
* @returns Formatted date string, or empty string if invalid
|
||||
*/
|
||||
export function formatDate(
|
||||
date: Date | string | number,
|
||||
formatStr: string = DEFAULT_DATE_FORMAT,
|
||||
options?: { locale?: Locale }
|
||||
): string {
|
||||
const parsed = normalizeDate(date)
|
||||
|
||||
if (!parsed || !isValid(parsed)) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return format(parsed, formatStr, options)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a human-readable relative time string (e.g. "3 days ago").
|
||||
*
|
||||
* @param date - A Date object, ISO string, or timestamp
|
||||
* @param options - Optional settings for suffix and locale
|
||||
* @returns Relative time string, or empty string if invalid
|
||||
*/
|
||||
export function relativeTime(
|
||||
date: Date | string | number,
|
||||
options?: { addSuffix?: boolean; locale?: Locale }
|
||||
): string {
|
||||
const parsed = normalizeDate(date)
|
||||
|
||||
if (!parsed || !isValid(parsed)) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return formatDistanceToNow(parsed, {
|
||||
addSuffix: options?.addSuffix ?? true,
|
||||
locale: options?.locale,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a date string or value into a Date object.
|
||||
*
|
||||
* @param date - A Date object, ISO string, or timestamp
|
||||
* @returns A valid Date object, or null if parsing fails
|
||||
*/
|
||||
export function parseDate(date: Date | string | number): Date | null {
|
||||
const parsed = normalizeDate(date)
|
||||
|
||||
if (!parsed || !isValid(parsed)) {
|
||||
return null
|
||||
}
|
||||
|
||||
return parsed
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a given date value is valid.
|
||||
*
|
||||
* @param date - A Date object, ISO string, or timestamp
|
||||
* @returns True if the date is valid
|
||||
*/
|
||||
export function isValidDate(date: Date | string | number): boolean {
|
||||
const parsed = normalizeDate(date)
|
||||
return parsed !== null && isValid(parsed)
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize various date input types into a Date object.
|
||||
*/
|
||||
function normalizeDate(date: Date | string | number): Date | null {
|
||||
if (date instanceof Date) {
|
||||
return date
|
||||
}
|
||||
|
||||
if (typeof date === 'string') {
|
||||
const parsed = parseISO(date)
|
||||
return isValid(parsed) ? parsed : null
|
||||
}
|
||||
|
||||
if (typeof date === 'number') {
|
||||
const parsed = new Date(date)
|
||||
return isValid(parsed) ? parsed : null
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
96
resources/scripts-v2/utils/format-money.ts
Normal file
96
resources/scripts-v2/utils/format-money.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
export interface CurrencyConfig {
|
||||
precision: number
|
||||
thousand_separator: string
|
||||
decimal_separator: string
|
||||
symbol: string
|
||||
swap_currency_symbol?: boolean
|
||||
}
|
||||
|
||||
const DEFAULT_CURRENCY: CurrencyConfig = {
|
||||
precision: 2,
|
||||
thousand_separator: ',',
|
||||
decimal_separator: '.',
|
||||
symbol: '$',
|
||||
swap_currency_symbol: false,
|
||||
}
|
||||
|
||||
/**
|
||||
* Format an amount in cents to a currency string with symbol and separators.
|
||||
*
|
||||
* @param amountInCents - The amount in cents (e.g. 10050 = $100.50)
|
||||
* @param currency - Currency configuration for formatting
|
||||
* @returns Formatted currency string (e.g. "$ 100.50")
|
||||
*/
|
||||
export function formatMoney(
|
||||
amountInCents: number,
|
||||
currency: CurrencyConfig = DEFAULT_CURRENCY
|
||||
): string {
|
||||
let amount = amountInCents / 100
|
||||
|
||||
const {
|
||||
symbol,
|
||||
swap_currency_symbol = false,
|
||||
} = currency
|
||||
|
||||
let precision = Math.abs(currency.precision)
|
||||
if (Number.isNaN(precision)) {
|
||||
precision = 2
|
||||
}
|
||||
|
||||
const negativeSign = amount < 0 ? '-' : ''
|
||||
amount = Math.abs(Number(amount) || 0)
|
||||
|
||||
const fixedAmount = amount.toFixed(precision)
|
||||
const integerPart = parseInt(fixedAmount, 10).toString()
|
||||
const remainder = integerPart.length > 3 ? integerPart.length % 3 : 0
|
||||
|
||||
const thousandText = remainder
|
||||
? integerPart.substring(0, remainder) + currency.thousand_separator
|
||||
: ''
|
||||
|
||||
const amountText = integerPart
|
||||
.substring(remainder)
|
||||
.replace(/(\d{3})(?=\d)/g, '$1' + currency.thousand_separator)
|
||||
|
||||
const precisionText = precision
|
||||
? currency.decimal_separator +
|
||||
Math.abs(amount - parseInt(fixedAmount, 10))
|
||||
.toFixed(precision)
|
||||
.slice(2)
|
||||
: ''
|
||||
|
||||
const combinedAmountText =
|
||||
negativeSign + thousandText + amountText + precisionText
|
||||
|
||||
const moneySymbol = `${symbol}`
|
||||
|
||||
return swap_currency_symbol
|
||||
? `${combinedAmountText} ${moneySymbol}`
|
||||
: `${moneySymbol} ${combinedAmountText}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a formatted currency string back to cents.
|
||||
*
|
||||
* @param formattedAmount - The formatted string (e.g. "$ 1,234.56")
|
||||
* @param currency - Currency configuration used for parsing
|
||||
* @returns Amount in cents
|
||||
*/
|
||||
export function parseMoneyCents(
|
||||
formattedAmount: string,
|
||||
currency: CurrencyConfig = DEFAULT_CURRENCY
|
||||
): number {
|
||||
const cleaned = formattedAmount
|
||||
.replace(currency.symbol, '')
|
||||
.replace(new RegExp(`\\${currency.thousand_separator}`, 'g'), '')
|
||||
.replace(currency.decimal_separator, '.')
|
||||
.trim()
|
||||
|
||||
const parsed = parseFloat(cleaned)
|
||||
|
||||
if (Number.isNaN(parsed)) {
|
||||
return 0
|
||||
}
|
||||
|
||||
return Math.round(parsed * 100)
|
||||
}
|
||||
29
resources/scripts-v2/utils/index.ts
Normal file
29
resources/scripts-v2/utils/index.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
export {
|
||||
formatMoney,
|
||||
parseMoneyCents,
|
||||
} from './format-money'
|
||||
export type { CurrencyConfig } from './format-money'
|
||||
|
||||
export {
|
||||
formatDate,
|
||||
relativeTime,
|
||||
parseDate,
|
||||
isValidDate,
|
||||
DEFAULT_DATE_FORMAT,
|
||||
DEFAULT_DATETIME_FORMAT,
|
||||
} from './format-date'
|
||||
|
||||
export {
|
||||
get as lsGet,
|
||||
set as lsSet,
|
||||
remove as lsRemove,
|
||||
has as lsHas,
|
||||
clear as lsClear,
|
||||
} from './local-storage'
|
||||
|
||||
export {
|
||||
handleApiError,
|
||||
extractValidationErrors,
|
||||
getErrorTranslationKey,
|
||||
} from './error-handling'
|
||||
export type { NormalizedApiError } from './error-handling'
|
||||
66
resources/scripts-v2/utils/local-storage.ts
Normal file
66
resources/scripts-v2/utils/local-storage.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* Typed wrapper around localStorage for safe get/set/remove operations.
|
||||
* Handles JSON serialization and deserialization automatically.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Retrieve a value from localStorage, parsed from JSON.
|
||||
*
|
||||
* @param key - The localStorage key
|
||||
* @returns The parsed value, or null if the key does not exist or parsing fails
|
||||
*/
|
||||
export function get<T>(key: string): T | null {
|
||||
const raw = localStorage.getItem(key)
|
||||
|
||||
if (raw === null) {
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(raw) as T
|
||||
} catch {
|
||||
// If parsing fails, return the raw string cast to T.
|
||||
// This handles cases where the value is a plain string not wrapped in quotes.
|
||||
return raw as unknown as T
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a value in localStorage as JSON.
|
||||
*
|
||||
* @param key - The localStorage key
|
||||
* @param value - The value to store (will be JSON-serialized)
|
||||
*/
|
||||
export function set<T>(key: string, value: T): void {
|
||||
if (typeof value === 'string') {
|
||||
localStorage.setItem(key, value)
|
||||
} else {
|
||||
localStorage.setItem(key, JSON.stringify(value))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a key from localStorage.
|
||||
*
|
||||
* @param key - The localStorage key to remove
|
||||
*/
|
||||
export function remove(key: string): void {
|
||||
localStorage.removeItem(key)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a key exists in localStorage.
|
||||
*
|
||||
* @param key - The localStorage key
|
||||
* @returns True if the key exists
|
||||
*/
|
||||
export function has(key: string): boolean {
|
||||
return localStorage.getItem(key) !== null
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all entries in localStorage.
|
||||
*/
|
||||
export function clear(): void {
|
||||
localStorage.clear()
|
||||
}
|
||||
Reference in New Issue
Block a user