feat: Hotfix Part1 - Backend support for company postal code, service tiers, timesheet naming
- Add postal_code column to companies table
- Add pricing_tiers column to services table for tiered pricing
- Update timesheet upload to generate filename in format {firstname}-{lastname}-timesheet-{date}
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2
src/db/migrations/0008_add_company_postal_code.sql
Normal file
2
src/db/migrations/0008_add_company_postal_code.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
-- Add postal_code column to companies table
|
||||||
|
ALTER TABLE "companies" ADD COLUMN IF NOT EXISTS "postal_code" text;
|
||||||
2
src/db/migrations/0009_add_service_pricing_tiers.sql
Normal file
2
src/db/migrations/0009_add_service_pricing_tiers.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
-- Add pricing_tiers column to services table for tiered pricing
|
||||||
|
ALTER TABLE "services" ADD COLUMN IF NOT EXISTS "pricing_tiers" text;
|
||||||
@@ -126,6 +126,7 @@ export const companies = pgTable('companies', {
|
|||||||
description: text('description'),
|
description: text('description'),
|
||||||
address: text('address'),
|
address: text('address'),
|
||||||
city: text('city'),
|
city: text('city'),
|
||||||
|
postalCode: text('postal_code'),
|
||||||
country: text('country'),
|
country: text('country'),
|
||||||
phone: text('phone'),
|
phone: text('phone'),
|
||||||
email: text('email'),
|
email: text('email'),
|
||||||
@@ -320,6 +321,7 @@ export const services = pgTable('services', {
|
|||||||
id: uuid('id').primaryKey().defaultRandom(),
|
id: uuid('id').primaryKey().defaultRandom(),
|
||||||
name: text('name').notNull(),
|
name: text('name').notNull(),
|
||||||
price: text('price').notNull(), // stored as text for flexibility with decimal
|
price: text('price').notNull(), // stored as text for flexibility with decimal
|
||||||
|
pricingTiers: text('pricing_tiers'), // JSON array of tiers: [{tier: "Silver", price: "€149"}, ...]
|
||||||
description: text('description'),
|
description: text('description'),
|
||||||
createdBy: uuid('created_by').references(() => users.id, { onDelete: 'set null' }),
|
createdBy: uuid('created_by').references(() => users.id, { onDelete: 'set null' }),
|
||||||
createdAt: timestamp('created_at').defaultNow().notNull(),
|
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||||
|
|||||||
@@ -76,16 +76,41 @@ const safeUnlink = async (filePath) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const generateTimesheetFileName = (firstName, lastName, year, month, fileExt) => {
|
||||||
|
const cleanFirstName = (firstName || 'user').toLowerCase().replace(/\s+/g, '-');
|
||||||
|
const cleanLastName = (lastName || '').toLowerCase().replace(/\s+/g, '-');
|
||||||
|
const monthStr = String(month).padStart(2, '0');
|
||||||
|
const namePrefix = cleanLastName ? `${cleanFirstName}-${cleanLastName}` : cleanFirstName;
|
||||||
|
return `${namePrefix}-timesheet-${year}-${monthStr}${fileExt}`;
|
||||||
|
};
|
||||||
|
|
||||||
export const uploadTimesheet = async ({ userId, year, month, file }) => {
|
export const uploadTimesheet = async ({ userId, year, month, file }) => {
|
||||||
if (!file) {
|
if (!file) {
|
||||||
throw new BadRequestError('Súbor nebol nahraný');
|
throw new BadRequestError('Súbor nebol nahraný');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fetch user info for filename generation
|
||||||
|
const [user] = await db
|
||||||
|
.select({ firstName: users.firstName, lastName: users.lastName })
|
||||||
|
.from(users)
|
||||||
|
.where(eq(users.id, userId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
const parsedYear = parseInt(year);
|
const parsedYear = parseInt(year);
|
||||||
const parsedMonth = parseInt(month);
|
const parsedMonth = parseInt(month);
|
||||||
const fileType = detectFileType(file.mimetype);
|
const fileType = detectFileType(file.mimetype);
|
||||||
|
const fileExt = path.extname(file.originalname);
|
||||||
const { folder, filename, filePath } = buildDestinationPath(userId, parsedYear, parsedMonth, file.originalname);
|
const { folder, filename, filePath } = buildDestinationPath(userId, parsedYear, parsedMonth, file.originalname);
|
||||||
|
|
||||||
|
// Generate user-friendly filename
|
||||||
|
const displayFileName = generateTimesheetFileName(
|
||||||
|
user?.firstName,
|
||||||
|
user?.lastName,
|
||||||
|
parsedYear,
|
||||||
|
parsedMonth,
|
||||||
|
fileExt
|
||||||
|
);
|
||||||
|
|
||||||
await fs.mkdir(folder, { recursive: true });
|
await fs.mkdir(folder, { recursive: true });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -95,7 +120,7 @@ export const uploadTimesheet = async ({ userId, year, month, file }) => {
|
|||||||
.insert(timesheets)
|
.insert(timesheets)
|
||||||
.values({
|
.values({
|
||||||
userId,
|
userId,
|
||||||
fileName: file.originalname,
|
fileName: displayFileName,
|
||||||
filePath,
|
filePath,
|
||||||
fileType,
|
fileType,
|
||||||
fileSize: file.size,
|
fileSize: file.size,
|
||||||
|
|||||||
Reference in New Issue
Block a user