pacioli 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +340 -0
- package/config/freelancer.example.json +16 -0
- package/customers/acme-corp.json +7 -0
- package/customers/demo-company.json +7 -0
- package/customers/test-customer.json +7 -0
- package/examples/invoice-auto.json +34 -0
- package/examples/invoice.json +34 -0
- package/examples/quotation-auto.json +34 -0
- package/examples/quotation.json +34 -0
- package/examples/receipt-auto.json +37 -0
- package/examples/receipt.json +37 -0
- package/package.json +58 -0
- package/src/cli.ts +111 -0
- package/src/commands/generate.ts +286 -0
- package/src/commands/init.ts +372 -0
- package/src/generator.ts +275 -0
- package/src/index.ts +250 -0
- package/src/metadata.ts +174 -0
- package/src/utils.ts +110 -0
- package/src/validator.ts +322 -0
- package/templates/invoice.html +389 -0
- package/templates/quotation.html +389 -0
- package/templates/receipt.html +397 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CLI Invoice Generator
|
|
5
|
+
* Main entry point
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { fileExists, readJSON, getOutputPath } from "./utils";
|
|
9
|
+
import {
|
|
10
|
+
validateInvoice,
|
|
11
|
+
validateQuotation,
|
|
12
|
+
validateReceipt,
|
|
13
|
+
validateFreelancerConfig,
|
|
14
|
+
validateCustomer,
|
|
15
|
+
type DocumentData,
|
|
16
|
+
type FreelancerConfig,
|
|
17
|
+
type Customer,
|
|
18
|
+
} from "./validator";
|
|
19
|
+
import { generatePDF } from "./generator";
|
|
20
|
+
import {
|
|
21
|
+
getNextDocumentNumber,
|
|
22
|
+
incrementDocumentCounter,
|
|
23
|
+
} from "./metadata";
|
|
24
|
+
|
|
25
|
+
const VALID_TYPES = ["invoice", "quotation", "receipt"] as const;
|
|
26
|
+
type DocumentType = (typeof VALID_TYPES)[number];
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Print usage information
|
|
30
|
+
*/
|
|
31
|
+
function printUsage() {
|
|
32
|
+
console.log(`
|
|
33
|
+
CLI Invoice Generator
|
|
34
|
+
|
|
35
|
+
Usage:
|
|
36
|
+
bun run generate <type> <input-json> --customer <customer-json> [options]
|
|
37
|
+
|
|
38
|
+
Arguments:
|
|
39
|
+
<type> Document type: invoice, quotation, or receipt
|
|
40
|
+
<input-json> Path to JSON data file
|
|
41
|
+
--customer Path to customer JSON file (required)
|
|
42
|
+
|
|
43
|
+
Options:
|
|
44
|
+
--output <path> Custom output PDF path (default: output/{type}-{number}.pdf)
|
|
45
|
+
--profile <path> Path to freelancer profile (default: config/freelancer.json)
|
|
46
|
+
--help Show this help message
|
|
47
|
+
|
|
48
|
+
Examples:
|
|
49
|
+
bun run generate invoice data/invoice-001.json --customer customers/acme-corp.json
|
|
50
|
+
bun run generate quotation data/quote-001.json --customer customers/demo.json --output custom/path.pdf
|
|
51
|
+
bun run generate receipt data/receipt-001.json --customer customers/test.json --profile config/freelancer.json
|
|
52
|
+
`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Parse command line arguments
|
|
57
|
+
*/
|
|
58
|
+
function parseArgs(args: string[]) {
|
|
59
|
+
const options: {
|
|
60
|
+
type?: DocumentType;
|
|
61
|
+
inputPath?: string;
|
|
62
|
+
customerPath?: string;
|
|
63
|
+
outputPath?: string;
|
|
64
|
+
configPath: string;
|
|
65
|
+
help: boolean;
|
|
66
|
+
} = {
|
|
67
|
+
configPath: "config/freelancer.json",
|
|
68
|
+
help: false,
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// Check for help flag
|
|
72
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
73
|
+
options.help = true;
|
|
74
|
+
return options;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Get positional arguments
|
|
78
|
+
const positionalArgs = args.filter((arg) => !arg.startsWith("--"));
|
|
79
|
+
|
|
80
|
+
if (positionalArgs.length >= 2) {
|
|
81
|
+
const type = positionalArgs[0];
|
|
82
|
+
if (VALID_TYPES.includes(type as DocumentType)) {
|
|
83
|
+
options.type = type as DocumentType;
|
|
84
|
+
}
|
|
85
|
+
options.inputPath = positionalArgs[1];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Parse options
|
|
89
|
+
for (let i = 0; i < args.length; i++) {
|
|
90
|
+
const arg = args[i];
|
|
91
|
+
const nextArg = args[i + 1];
|
|
92
|
+
|
|
93
|
+
if (arg === "--customer" && nextArg) {
|
|
94
|
+
options.customerPath = nextArg;
|
|
95
|
+
i++;
|
|
96
|
+
} else if (arg === "--output" && nextArg) {
|
|
97
|
+
options.outputPath = nextArg;
|
|
98
|
+
i++;
|
|
99
|
+
} else if (arg === "--profile" && nextArg) {
|
|
100
|
+
options.configPath = nextArg;
|
|
101
|
+
i++;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return options;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Main function
|
|
110
|
+
*/
|
|
111
|
+
async function main() {
|
|
112
|
+
const args = process.argv.slice(2);
|
|
113
|
+
|
|
114
|
+
if (args.length === 0) {
|
|
115
|
+
printUsage();
|
|
116
|
+
process.exit(0);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const options = parseArgs(args);
|
|
120
|
+
|
|
121
|
+
if (options.help) {
|
|
122
|
+
printUsage();
|
|
123
|
+
process.exit(0);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Validate arguments
|
|
127
|
+
if (!options.type) {
|
|
128
|
+
console.error("Error: Invalid or missing document type");
|
|
129
|
+
console.error(
|
|
130
|
+
`Valid types: ${VALID_TYPES.join(", ")}`
|
|
131
|
+
);
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (!options.inputPath) {
|
|
136
|
+
console.error("Error: Input JSON file path is required");
|
|
137
|
+
printUsage();
|
|
138
|
+
process.exit(1);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (!options.customerPath) {
|
|
142
|
+
console.error("Error: Customer JSON file path is required (use --customer)");
|
|
143
|
+
printUsage();
|
|
144
|
+
process.exit(1);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Check if input file exists
|
|
148
|
+
if (!(await fileExists(options.inputPath))) {
|
|
149
|
+
console.error(`Error: Input file not found: ${options.inputPath}`);
|
|
150
|
+
process.exit(1);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Check if customer file exists
|
|
154
|
+
if (!(await fileExists(options.customerPath))) {
|
|
155
|
+
console.error(`Error: Customer file not found: ${options.customerPath}`);
|
|
156
|
+
process.exit(1);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Check if profile file exists
|
|
160
|
+
if (!(await fileExists(options.configPath))) {
|
|
161
|
+
console.error(`Error: Profile file not found: ${options.configPath}`);
|
|
162
|
+
console.error(
|
|
163
|
+
"Tip: Copy config/freelancer.example.json to config/freelancer.json"
|
|
164
|
+
);
|
|
165
|
+
process.exit(1);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
console.log(`📄 Generating ${options.type}...`);
|
|
170
|
+
|
|
171
|
+
// Load freelancer profile
|
|
172
|
+
console.log(`📋 Loading profile from ${options.configPath}...`);
|
|
173
|
+
const config = await readJSON<FreelancerConfig>(options.configPath);
|
|
174
|
+
|
|
175
|
+
// Validate profile
|
|
176
|
+
const configValidation = validateFreelancerConfig(config);
|
|
177
|
+
if (!configValidation.valid) {
|
|
178
|
+
console.error("Error: Invalid freelancer profile:");
|
|
179
|
+
configValidation.errors.forEach((err) => console.error(` - ${err}`));
|
|
180
|
+
process.exit(1);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Load customer data
|
|
184
|
+
console.log(`👤 Loading customer from ${options.customerPath}...`);
|
|
185
|
+
const customer = await readJSON<Customer>(options.customerPath);
|
|
186
|
+
|
|
187
|
+
// Validate customer
|
|
188
|
+
const customerValidation = validateCustomer(customer);
|
|
189
|
+
if (!customerValidation.valid) {
|
|
190
|
+
console.error("Error: Invalid customer data:");
|
|
191
|
+
customerValidation.errors.forEach((err) => console.error(` - ${err}`));
|
|
192
|
+
process.exit(1);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Load document data
|
|
196
|
+
console.log(`📋 Loading data from ${options.inputPath}...`);
|
|
197
|
+
const data = await readJSON<DocumentData>(options.inputPath);
|
|
198
|
+
|
|
199
|
+
// Validate document data based on type
|
|
200
|
+
let validation;
|
|
201
|
+
switch (options.type) {
|
|
202
|
+
case "invoice":
|
|
203
|
+
validation = validateInvoice(data);
|
|
204
|
+
break;
|
|
205
|
+
case "quotation":
|
|
206
|
+
validation = validateQuotation(data);
|
|
207
|
+
break;
|
|
208
|
+
case "receipt":
|
|
209
|
+
validation = validateReceipt(data);
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (!validation.valid) {
|
|
214
|
+
console.error(`Error: Invalid ${options.type} data:`);
|
|
215
|
+
validation.errors.forEach((err) => console.error(` - ${err}`));
|
|
216
|
+
process.exit(1);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Handle auto-numbering
|
|
220
|
+
let resolvedDocumentNumber = data.documentNumber;
|
|
221
|
+
if (data.documentNumber === "auto") {
|
|
222
|
+
console.log(`🔢 Generating next document number...`);
|
|
223
|
+
resolvedDocumentNumber = await getNextDocumentNumber(options.type);
|
|
224
|
+
data.documentNumber = resolvedDocumentNumber;
|
|
225
|
+
console.log(`✓ Document number: ${resolvedDocumentNumber}`);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Determine output path
|
|
229
|
+
const outputPath = getOutputPath(
|
|
230
|
+
options.type,
|
|
231
|
+
resolvedDocumentNumber,
|
|
232
|
+
options.outputPath
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
// Generate PDF
|
|
236
|
+
console.log(`🔨 Generating PDF...`);
|
|
237
|
+
await generatePDF(options.type, data, customer, config, outputPath);
|
|
238
|
+
|
|
239
|
+
// Update metadata counter after successful generation
|
|
240
|
+
await incrementDocumentCounter(options.type, resolvedDocumentNumber);
|
|
241
|
+
|
|
242
|
+
console.log(`\n✅ Success! PDF saved to: ${outputPath}`);
|
|
243
|
+
} catch (error) {
|
|
244
|
+
console.error("Error:", error instanceof Error ? error.message : error);
|
|
245
|
+
process.exit(1);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Run main
|
|
250
|
+
main();
|
package/src/metadata.ts
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Document metadata management for auto-numbering
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { fileExists } from "./utils";
|
|
6
|
+
|
|
7
|
+
export interface DocumentTypeMetadata {
|
|
8
|
+
lastNumber: number;
|
|
9
|
+
prefix: string;
|
|
10
|
+
year: number;
|
|
11
|
+
month: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface Metadata {
|
|
15
|
+
invoice: DocumentTypeMetadata;
|
|
16
|
+
quotation: DocumentTypeMetadata;
|
|
17
|
+
receipt: DocumentTypeMetadata;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const METADATA_PATH = ".metadata.json";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Get default metadata structure
|
|
24
|
+
*/
|
|
25
|
+
function getDefaultMetadata(): Metadata {
|
|
26
|
+
const now = new Date();
|
|
27
|
+
const currentYear = now.getFullYear();
|
|
28
|
+
const currentMonth = now.getMonth() + 1; // 1-12
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
invoice: {
|
|
32
|
+
lastNumber: 0,
|
|
33
|
+
prefix: "INV",
|
|
34
|
+
year: currentYear,
|
|
35
|
+
month: currentMonth,
|
|
36
|
+
},
|
|
37
|
+
quotation: {
|
|
38
|
+
lastNumber: 0,
|
|
39
|
+
prefix: "QT",
|
|
40
|
+
year: currentYear,
|
|
41
|
+
month: currentMonth,
|
|
42
|
+
},
|
|
43
|
+
receipt: {
|
|
44
|
+
lastNumber: 0,
|
|
45
|
+
prefix: "REC",
|
|
46
|
+
year: currentYear,
|
|
47
|
+
month: currentMonth,
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Read metadata from file
|
|
54
|
+
*/
|
|
55
|
+
export async function readMetadata(): Promise<Metadata> {
|
|
56
|
+
try {
|
|
57
|
+
if (await fileExists(METADATA_PATH)) {
|
|
58
|
+
const file = Bun.file(METADATA_PATH);
|
|
59
|
+
const data = await file.json();
|
|
60
|
+
|
|
61
|
+
// Validate and return metadata
|
|
62
|
+
return data as Metadata;
|
|
63
|
+
}
|
|
64
|
+
} catch (error) {
|
|
65
|
+
console.warn("Warning: Could not read metadata file, using defaults");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return getDefaultMetadata();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Write metadata to file
|
|
73
|
+
*/
|
|
74
|
+
export async function writeMetadata(metadata: Metadata): Promise<void> {
|
|
75
|
+
try {
|
|
76
|
+
await Bun.write(METADATA_PATH, JSON.stringify(metadata, null, 2));
|
|
77
|
+
} catch (error) {
|
|
78
|
+
throw new Error(`Failed to write metadata: ${error}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Generate next document number for a given type
|
|
84
|
+
*/
|
|
85
|
+
export async function getNextDocumentNumber(
|
|
86
|
+
type: "invoice" | "quotation" | "receipt"
|
|
87
|
+
): Promise<string> {
|
|
88
|
+
const metadata = await readMetadata();
|
|
89
|
+
const typeMetadata = metadata[type];
|
|
90
|
+
const now = new Date();
|
|
91
|
+
const currentYear = now.getFullYear();
|
|
92
|
+
const currentMonth = now.getMonth() + 1; // 1-12
|
|
93
|
+
|
|
94
|
+
// Reset counter if year or month has changed
|
|
95
|
+
if (typeMetadata.year !== currentYear || typeMetadata.month !== currentMonth) {
|
|
96
|
+
typeMetadata.year = currentYear;
|
|
97
|
+
typeMetadata.month = currentMonth;
|
|
98
|
+
typeMetadata.lastNumber = 0;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Increment counter
|
|
102
|
+
const nextNumber = typeMetadata.lastNumber + 1;
|
|
103
|
+
|
|
104
|
+
// Format: PREFIX-YYYYMM-00N (e.g., INV-202410-001)
|
|
105
|
+
const yearMonth = `${currentYear}${String(currentMonth).padStart(2, "0")}`;
|
|
106
|
+
const documentNumber = `${typeMetadata.prefix}-${yearMonth}-${String(nextNumber).padStart(3, "0")}`;
|
|
107
|
+
|
|
108
|
+
return documentNumber;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Update metadata after successful document generation
|
|
113
|
+
*/
|
|
114
|
+
export async function incrementDocumentCounter(
|
|
115
|
+
type: "invoice" | "quotation" | "receipt",
|
|
116
|
+
documentNumber: string
|
|
117
|
+
): Promise<void> {
|
|
118
|
+
const metadata = await readMetadata();
|
|
119
|
+
const typeMetadata = metadata[type];
|
|
120
|
+
const now = new Date();
|
|
121
|
+
const currentYear = now.getFullYear();
|
|
122
|
+
const currentMonth = now.getMonth() + 1; // 1-12
|
|
123
|
+
|
|
124
|
+
// Reset counter if year or month has changed
|
|
125
|
+
if (typeMetadata.year !== currentYear || typeMetadata.month !== currentMonth) {
|
|
126
|
+
typeMetadata.year = currentYear;
|
|
127
|
+
typeMetadata.month = currentMonth;
|
|
128
|
+
typeMetadata.lastNumber = 0;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Extract number from document number (e.g., "INV-202410-005" -> 5)
|
|
132
|
+
const match = documentNumber.match(/-(\d+)$/);
|
|
133
|
+
if (match && match[1]) {
|
|
134
|
+
const number = parseInt(match[1], 10);
|
|
135
|
+
// Only update if this number is higher than current
|
|
136
|
+
if (number > typeMetadata.lastNumber) {
|
|
137
|
+
typeMetadata.lastNumber = number;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
await writeMetadata(metadata);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Initialize metadata file if it doesn't exist
|
|
146
|
+
*/
|
|
147
|
+
export async function initMetadata(): Promise<void> {
|
|
148
|
+
if (!(await fileExists(METADATA_PATH))) {
|
|
149
|
+
const defaultMetadata = getDefaultMetadata();
|
|
150
|
+
await writeMetadata(defaultMetadata);
|
|
151
|
+
console.log("✓ Created .metadata.json with default values");
|
|
152
|
+
} else {
|
|
153
|
+
console.log("✓ .metadata.json already exists");
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Reset counter for a specific document type
|
|
159
|
+
*/
|
|
160
|
+
export async function resetCounter(
|
|
161
|
+
type: "invoice" | "quotation" | "receipt"
|
|
162
|
+
): Promise<void> {
|
|
163
|
+
const metadata = await readMetadata();
|
|
164
|
+
const now = new Date();
|
|
165
|
+
const currentYear = now.getFullYear();
|
|
166
|
+
const currentMonth = now.getMonth() + 1; // 1-12
|
|
167
|
+
|
|
168
|
+
metadata[type].lastNumber = 0;
|
|
169
|
+
metadata[type].year = currentYear;
|
|
170
|
+
metadata[type].month = currentMonth;
|
|
171
|
+
|
|
172
|
+
await writeMetadata(metadata);
|
|
173
|
+
console.log(`✓ Reset ${type} counter to 0 for ${currentYear}-${String(currentMonth).padStart(2, "0")}`);
|
|
174
|
+
}
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility functions for invoice generation
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface LineItem {
|
|
6
|
+
description: string;
|
|
7
|
+
quantity: number;
|
|
8
|
+
unit: string;
|
|
9
|
+
unitPrice: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface CalculationResult {
|
|
13
|
+
subtotal: number;
|
|
14
|
+
taxAmount: number;
|
|
15
|
+
total: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Calculate subtotal, tax, and total from line items
|
|
20
|
+
*/
|
|
21
|
+
export function calculateTotals(
|
|
22
|
+
items: LineItem[],
|
|
23
|
+
taxRate: number,
|
|
24
|
+
taxType: "withholding" | "vat"
|
|
25
|
+
): CalculationResult {
|
|
26
|
+
const subtotal = items.reduce(
|
|
27
|
+
(sum, item) => sum + item.quantity * item.unitPrice,
|
|
28
|
+
0
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
const taxAmount = subtotal * taxRate;
|
|
32
|
+
|
|
33
|
+
let total: number;
|
|
34
|
+
if (taxType === "withholding") {
|
|
35
|
+
// Withholding tax is deducted from subtotal
|
|
36
|
+
total = subtotal - taxAmount;
|
|
37
|
+
} else {
|
|
38
|
+
// VAT is added to subtotal
|
|
39
|
+
total = subtotal + taxAmount;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
subtotal,
|
|
44
|
+
taxAmount,
|
|
45
|
+
total,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Format number with Thai thousand separators
|
|
51
|
+
*/
|
|
52
|
+
export function formatNumber(num: number, decimals: number = 2): string {
|
|
53
|
+
return num.toFixed(decimals).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Convert Gregorian date to Buddhist Era (BE) format
|
|
58
|
+
*/
|
|
59
|
+
export function formatDateThai(dateString: string): string {
|
|
60
|
+
const date = new Date(dateString);
|
|
61
|
+
|
|
62
|
+
const thaiMonths = [
|
|
63
|
+
"มกราคม", "กุมภาพันธ์", "มีนาคม", "เมษายน",
|
|
64
|
+
"พฤษภาคม", "มิถุนายน", "กรกฎาคม", "สิงหาคม",
|
|
65
|
+
"กันยายน", "ตุลาคม", "พฤศจิกายน", "ธันวาคม"
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
const day = date.getDate();
|
|
69
|
+
const month = thaiMonths[date.getMonth()];
|
|
70
|
+
const yearBE = date.getFullYear() + 543; // Convert to Buddhist Era
|
|
71
|
+
|
|
72
|
+
return `${day} ${month} ${yearBE}`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Validate file path exists
|
|
77
|
+
*/
|
|
78
|
+
export async function fileExists(path: string): Promise<boolean> {
|
|
79
|
+
try {
|
|
80
|
+
const file = Bun.file(path);
|
|
81
|
+
return await file.exists();
|
|
82
|
+
} catch {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Read and parse JSON file
|
|
89
|
+
*/
|
|
90
|
+
export async function readJSON<T>(path: string): Promise<T> {
|
|
91
|
+
const file = Bun.file(path);
|
|
92
|
+
return await file.json();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Get output file path
|
|
97
|
+
*/
|
|
98
|
+
export function getOutputPath(
|
|
99
|
+
type: string,
|
|
100
|
+
documentNumber: string,
|
|
101
|
+
customOutput?: string
|
|
102
|
+
): string {
|
|
103
|
+
if (customOutput) {
|
|
104
|
+
return customOutput;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Default: output/{type}-{number}.pdf
|
|
108
|
+
const filename = `${type}-${documentNumber}.pdf`;
|
|
109
|
+
return `output/${filename}`;
|
|
110
|
+
}
|