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:
richardtekula
2026-01-22 07:46:50 +01:00
parent 826fd467bc
commit a0a6656a49
4 changed files with 32 additions and 1 deletions

View File

@@ -0,0 +1,2 @@
-- Add postal_code column to companies table
ALTER TABLE "companies" ADD COLUMN IF NOT EXISTS "postal_code" text;

View 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;

View File

@@ -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(),

View File

@@ -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,