Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 101 additions & 0 deletions electron/database/repositories/credits.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import type {
IdempotentResult,
} from '../../../src/types/database';
import { incrementVersion } from '../../lib/dataVersions';
import { deleteSale } from './sales';

export function getAllCredits(status?: string): Credit[] {
const db = getDatabase();
Expand Down Expand Up @@ -152,6 +153,106 @@ export function getCreditPayments(creditId: number): CreditPayment[] {
).all(creditId) as CreditPayment[];
}

// Delete a credit and its associated sale (reverses stock)
export function deleteCredit(creditId: number): boolean {
const db = getDatabase();

const parsedId = Number(creditId);
if (!Number.isInteger(parsedId) || parsedId < 1) {
throw new Error('ID de credito invalido');
}

const credit = getCreditById(parsedId);
if (!credit) {
throw new Error('El credito no existe');
}

// Delegate to deleteSale which handles stock reversal, credit cleanup, and sale deletion
return deleteSale(credit.sale_id);
}

// Update credit fields: due_date and/or surcharge_percent
export function updateCredit(
creditId: number,
data: { due_date?: string; surcharge_percent?: number }
): Credit {
const db = getDatabase();

const parsedId = Number(creditId);
if (!Number.isInteger(parsedId) || parsedId < 1) {
throw new Error('ID de credito invalido');
}

const credit = getCreditById(parsedId);
if (!credit) {
throw new Error('El credito no existe');
}

if (credit.status === 'paid') {
throw new Error('No se puede editar un credito ya pagado');
}

const updates: string[] = [];
const params: unknown[] = [];

if (data.due_date !== undefined) {
const trimmedDate = data.due_date.trim();
if (!/^\d{4}-\d{2}-\d{2}$/.test(trimmedDate)) {
throw new Error('La fecha limite no tiene un formato valido');
}
const [year, month, day] = trimmedDate.split('-').map(Number);
const dateObj = new Date(year, month - 1, day, 0, 0, 0, 0);
if (
Number.isNaN(dateObj.getTime()) ||
dateObj.getFullYear() !== year ||
dateObj.getMonth() !== month - 1 ||
dateObj.getDate() !== day
) {
throw new Error('La fecha limite no es valida');
}
updates.push('due_date = ?');
params.push(trimmedDate);
}

if (data.surcharge_percent !== undefined) {
const percent = Number(data.surcharge_percent);
if (!Number.isFinite(percent) || percent < 0 || percent > 100) {
throw new Error('El porcentaje de recargo debe estar entre 0 y 100');
}
updates.push('surcharge_percent = ?');
params.push(percent);

// If surcharge has not been applied yet, update total_due to original_amount
// (surcharge will be recalculated when overdue check runs)
if (credit.surcharge_applied === 0) {
// total_due stays as original_amount; surcharge_percent just changes the future rate
} else {
// Recalculate total_due with new surcharge percent
const newTotalDue = roundMoney(credit.original_amount * (1 + percent / 100));
updates.push('total_due = ?');
params.push(newTotalDue);

// Also update the sale surcharge and total
const surchargeDiff = roundMoney(newTotalDue - credit.original_amount);
db.prepare(
'UPDATE sales SET surcharge = ?, total = subtotal + ? WHERE id = ?'
).run(surchargeDiff, surchargeDiff, credit.sale_id);
}
}

if (updates.length === 0) {
return credit;
}

params.push(parsedId);
db.prepare(
`UPDATE credits SET ${updates.join(', ')} WHERE id = ?`
).run(...params);

incrementVersion('credits');
return getCreditById(parsedId)!;
}

// Check and apply surcharges to overdue credits
export function checkOverdueCredits(): number {
const db = getDatabase();
Expand Down
10 changes: 9 additions & 1 deletion electron/database/repositories/sales.ts
Original file line number Diff line number Diff line change
Expand Up @@ -416,9 +416,17 @@ export function getSaleDetailById(id: number): SaleDetail | undefined {
s.*,
c.name AS customer_name,
c.phone AS customer_phone,
c.email AS customer_email
c.email AS customer_email,
cr.due_date AS credit_due_date,
cr.created_at AS credit_created_at,
CASE
WHEN cr.id IS NOT NULL
THEN CAST(ROUND(julianday(cr.due_date) - julianday(SUBSTR(cr.created_at, 1, 10))) AS INTEGER)
ELSE NULL
END AS credit_days
FROM sales s
LEFT JOIN customers c ON c.id = s.customer_id
LEFT JOIN credits cr ON cr.sale_id = s.id
WHERE s.id = ?`
).get(id) as Omit<SaleDetail, 'items'> | undefined;

Expand Down
27 changes: 27 additions & 0 deletions electron/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -841,6 +841,33 @@ function registerIpcHandlers(): void {
}
);

ipcMain.handle(IPC_CHANNELS.CREDITS_DELETE, (_, id: number | string) => {
const parsedId = Number(id);
if (!Number.isInteger(parsedId) || parsedId < 1) {
throw new Error('ID de credito invalido');
}
return creditsRepo.deleteCredit(parsedId);
});

ipcMain.handle(IPC_CHANNELS.CREDITS_UPDATE, (_, id: number | string, data: unknown) => {
const parsedId = Number(id);
if (!Number.isInteger(parsedId) || parsedId < 1) {
throw new Error('ID de credito invalido');
}

const d = (typeof data === 'object' && data !== null ? data : {}) as Record<string, unknown>;
const updateData: { due_date?: string; surcharge_percent?: number } = {};

if (typeof d.due_date === 'string') {
updateData.due_date = d.due_date.slice(0, 10);
}
if (typeof d.surcharge_percent === 'number') {
updateData.surcharge_percent = d.surcharge_percent;
}

return creditsRepo.updateCredit(parsedId, updateData);
});

// Inventory
ipcMain.handle(IPC_CHANNELS.INVENTORY_ADD_MOVEMENT, (_, data) => {
if (canUseCloudApi()) {
Expand Down
4 changes: 4 additions & 0 deletions electron/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,10 @@ const electronAPI = {
ipcRenderer.invoke(IPC_CHANNELS.CREDITS_GET_PAYMENTS_PAGINATED, creditId, query),
getSummary: (query: { search?: string; status?: string; dateFrom?: string; dateTo?: string }) =>
ipcRenderer.invoke(IPC_CHANNELS.CREDITS_GET_SUMMARY, query),
delete: (id: number) =>
ipcRenderer.invoke(IPC_CHANNELS.CREDITS_DELETE, id),
update: (id: number, data: { due_date?: string; surcharge_percent?: number }) =>
ipcRenderer.invoke(IPC_CHANNELS.CREDITS_UPDATE, id, data),
},

// Inventory
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "store-internal",
"private": true,
"version": "0.8.9",
"version": "0.8.10",
"license": "Apache-2.0",
"type": "module",
"main": "dist-electron/main.js",
Expand Down
34 changes: 34 additions & 0 deletions src/hooks/useCredits.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,38 @@ export function useCredits() {
}
}, []);

const deleteCredit = useCallback(async (creditId: number): Promise<boolean> => {
try {
setLoading(true);
const result = await window.electronAPI.credits.delete(creditId);
return result;
} catch (err) {
const message = err instanceof Error ? err.message : 'Error al eliminar credito';
console.error('useCredits.deleteCredit:', err);
throw new Error(message);
} finally {
setLoading(false);
}
}, []);

const updateCredit = useCallback(async (
creditId: number,
data: { due_date?: string; surcharge_percent?: number }
): Promise<Credit> => {
try {
setLoading(true);
const updated = await window.electronAPI.credits.update(creditId, data);
setCredits(prev => prev.map(c => c.id === creditId ? updated : c));
return updated;
} catch (err) {
const message = err instanceof Error ? err.message : 'Error al actualizar credito';
console.error('useCredits.updateCredit:', err);
throw new Error(message);
} finally {
setLoading(false);
}
}, []);

return {
credits,
loading,
Expand All @@ -164,5 +196,7 @@ export function useCredits() {
fetchCreditsByCustomerPaginated,
fetchPaymentsPaginated,
fetchSummary,
deleteCredit,
updateCredit,
};
}
2 changes: 2 additions & 0 deletions src/lib/ipcChannels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ export const IPC_CHANNELS = {
CREDITS_GET_BY_CUSTOMER_PAGINATED: 'credits:getByCustomerPaginated',
CREDITS_GET_PAYMENTS_PAGINATED: 'credits:getPaymentsPaginated',
CREDITS_GET_SUMMARY: 'credits:getSummary',
CREDITS_DELETE: 'credits:delete',
CREDITS_UPDATE: 'credits:update',

// Inventory
INVENTORY_ADD_MOVEMENT: 'inventory:addMovement',
Expand Down
21 changes: 21 additions & 0 deletions src/pages/CreditsPage.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,27 @@
background-color: rgba(26, 43, 60, 0.05);
}

.btn-danger {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
padding: var(--spacing-xs) var(--spacing-md);
background-color: transparent;
color: var(--color-error);
border: 1px solid var(--color-error);
border-radius: var(--radius-sm);
font-size: var(--font-size-sm);
font-family: var(--font-family);
font-weight: 500;
cursor: pointer;
transition: background-color var(--transition-fast);
}

.btn-danger:hover {
background-color: rgba(239, 68, 68, 0.05);
}

/* Notification */
.notification {
position: fixed;
Expand Down
Loading