make major bonus updates
This commit is contained in:
@@ -118,16 +118,22 @@
|
||||
}
|
||||
|
||||
function rewardLabel(r: any): string {
|
||||
if (r.rewardType === 'cash') return `£${(Number(r.value) / 100).toFixed(2)}`;
|
||||
if (r.rewardType === 'cash') return `£${Number(r.value).toFixed(2)}`;
|
||||
if (r.rewardType === 'points') return `${r.value} pts`;
|
||||
return r.label;
|
||||
}
|
||||
|
||||
function manualConfigs() {
|
||||
return parentBonusConfigs.filter(
|
||||
(b: any) =>
|
||||
(b.phase === 'active' || (!b.phase && b.status === 'active')) && b.type === 'manual'
|
||||
);
|
||||
return parentBonusConfigs.filter((b: any) => {
|
||||
if (!(b.status === 'active' && b.type === 'manual')) return false;
|
||||
if (b.occurrence === 'once') {
|
||||
const hasReward = parentRewards.some(
|
||||
(r: any) => r.bonusConfigId === b.id && (!b.memberId || r.memberId === b.memberId)
|
||||
);
|
||||
if (hasReward) return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Child View (kanban) ───
|
||||
@@ -140,11 +146,29 @@
|
||||
let nameInput = $state('');
|
||||
let showColorPicker = $state(false);
|
||||
|
||||
let templates = $derived(famStore.initialized ? (famStore.templates as ChoreTemplate[]) : (data.templates as ChoreTemplate[]));
|
||||
let assigned = $derived(famStore.initialized ? (famStore.assigned as AssignedChore[]) : (data.assigned as AssignedChore[]));
|
||||
let completions = $derived(famStore.initialized ? (famStore.completions as Completion[]) : (data.completions as Completion[]));
|
||||
let rewards = $derived(famStore.initialized ? (famStore.rewards as Reward[]) : (data.rewards as Reward[]));
|
||||
let bonusConfigs = $derived(famStore.initialized ? (famStore.bonusConfigs as BonusConfig[]) : (data.bonusConfigs as BonusConfig[]));
|
||||
let templates = $derived(
|
||||
famStore.initialized
|
||||
? (famStore.templates as ChoreTemplate[])
|
||||
: (data.templates as ChoreTemplate[])
|
||||
);
|
||||
let assigned = $derived(
|
||||
famStore.initialized
|
||||
? (famStore.assigned as AssignedChore[])
|
||||
: (data.assigned as AssignedChore[])
|
||||
);
|
||||
let completions = $derived(
|
||||
famStore.initialized
|
||||
? (famStore.completions as Completion[])
|
||||
: (data.completions as Completion[])
|
||||
);
|
||||
let rewards = $derived(
|
||||
famStore.initialized ? (famStore.rewards as Reward[]) : (data.rewards as Reward[])
|
||||
);
|
||||
let bonusConfigs = $derived(
|
||||
famStore.initialized
|
||||
? (famStore.bonusConfigs as BonusConfig[])
|
||||
: (data.bonusConfigs as BonusConfig[])
|
||||
);
|
||||
|
||||
let loading = $state(true);
|
||||
let error = $state('');
|
||||
@@ -187,8 +211,12 @@
|
||||
let completedToday = $derived(
|
||||
completions.filter((c) => c.memberId === memberId && c.date?.slice(0, 10) === todayChild)
|
||||
);
|
||||
let claimableRewardsChild = $derived(rewards.filter((r) => r.memberId === memberId && r.status === 'unclaimed'));
|
||||
let requestedRewardsChild = $derived(rewards.filter((r) => r.memberId === memberId && r.status === 'requested'));
|
||||
let claimableRewardsChild = $derived(
|
||||
rewards.filter((r) => r.memberId === memberId && r.status === 'unclaimed')
|
||||
);
|
||||
let requestedRewardsChild = $derived(
|
||||
rewards.filter((r) => r.memberId === memberId && r.status === 'requested')
|
||||
);
|
||||
|
||||
let weeklyTotal = $derived.by(() => {
|
||||
const dailyChores = memberChores.filter((a) => a.frequency === 'daily');
|
||||
@@ -211,9 +239,7 @@
|
||||
const chore = assigned.find((a) => a.id === c.assignedChoreId);
|
||||
return sum + (chore?.type === 'money' ? Number(chore.value) : 0);
|
||||
}, 0) +
|
||||
myRewards
|
||||
.filter((r) => r.rewardType === 'cash')
|
||||
.reduce((sum, r) => sum + Number(r.value), 0)
|
||||
myRewards.filter((r) => r.rewardType === 'cash').reduce((sum, r) => sum + Number(r.value), 0)
|
||||
);
|
||||
});
|
||||
|
||||
@@ -248,13 +274,12 @@
|
||||
const myCompletions = completions.filter((c) => c.memberId === memberId);
|
||||
const activeConfigs = bonusConfigs.filter(
|
||||
(b: BonusConfig) =>
|
||||
(b.phase === 'active' || (!b.phase && b.status === 'active')) &&
|
||||
b.status === 'active' &&
|
||||
b.target === 'individual' &&
|
||||
(!b.memberId || b.memberId === memberId)
|
||||
);
|
||||
return activeConfigs.map((cfg) => {
|
||||
const pStart =
|
||||
cfg.period === 'weekly' ? ws : cfg.period === 'monthly' ? monthStartStr() : '';
|
||||
const pStart = cfg.period === 'weekly' ? ws : cfg.period === 'monthly' ? monthStartStr() : '';
|
||||
const pEnd = pStart
|
||||
? (() => {
|
||||
if (cfg.period === 'weekly') {
|
||||
@@ -274,8 +299,7 @@
|
||||
const periodCompletions = pStart
|
||||
? myCompletions.filter(
|
||||
(c) =>
|
||||
(c.date?.slice(0, 10) || c.date) >= pStart &&
|
||||
(c.date?.slice(0, 10) || c.date) <= pEnd
|
||||
(c.date?.slice(0, 10) || c.date) >= pStart && (c.date?.slice(0, 10) || c.date) <= pEnd
|
||||
)
|
||||
: myCompletions;
|
||||
|
||||
@@ -374,14 +398,18 @@
|
||||
famStore.applyRecord('completions', existing, 'delete');
|
||||
}
|
||||
} else {
|
||||
famStore.applyRecord('completions', {
|
||||
id: 'optimistic-' + chore.id,
|
||||
famId,
|
||||
memberId,
|
||||
assignedChoreId: chore.id,
|
||||
date: todayChild,
|
||||
completedAt: new Date().toISOString()
|
||||
} as Completion, 'create');
|
||||
famStore.applyRecord(
|
||||
'completions',
|
||||
{
|
||||
id: 'optimistic-' + chore.id,
|
||||
famId,
|
||||
memberId,
|
||||
assignedChoreId: chore.id,
|
||||
date: todayChild,
|
||||
completedAt: new Date().toISOString()
|
||||
} as Completion,
|
||||
'create'
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -392,14 +420,18 @@
|
||||
}
|
||||
} catch (e) {
|
||||
if (wasCompleted) {
|
||||
famStore.applyRecord('completions', {
|
||||
id: 'revert-' + chore.id + '-' + Date.now(),
|
||||
famId,
|
||||
memberId,
|
||||
assignedChoreId: chore.id,
|
||||
date: todayChild,
|
||||
completedAt: new Date().toISOString()
|
||||
} as Completion, 'create');
|
||||
famStore.applyRecord(
|
||||
'completions',
|
||||
{
|
||||
id: 'revert-' + chore.id + '-' + Date.now(),
|
||||
famId,
|
||||
memberId,
|
||||
assignedChoreId: chore.id,
|
||||
date: todayChild,
|
||||
completedAt: new Date().toISOString()
|
||||
} as Completion,
|
||||
'create'
|
||||
);
|
||||
} else {
|
||||
const optimistic = completions.find((c) => c.id === 'optimistic-' + chore.id);
|
||||
if (optimistic) {
|
||||
@@ -421,7 +453,7 @@
|
||||
}
|
||||
|
||||
function rewardLabelChild(r: Reward): string {
|
||||
if (r.rewardType === 'cash') return `£${(Number(r.value) / 100).toFixed(2)}`;
|
||||
if (r.rewardType === 'cash') return `£${Number(r.value).toFixed(2)}`;
|
||||
if (r.rewardType === 'points') return `${r.value} pts`;
|
||||
return r.label;
|
||||
}
|
||||
@@ -468,7 +500,14 @@
|
||||
{#each todays as c}
|
||||
<li>
|
||||
✅ {choreNameFor(c.assignedChoreId)}
|
||||
<form method="POST" action="?/revoke" use:enhance={() => { return async (args) => handleResult(args); }} class="revoke-form">
|
||||
<form
|
||||
method="POST"
|
||||
action="?/revoke"
|
||||
use:enhance={() => {
|
||||
return async (args) => handleResult(args);
|
||||
}}
|
||||
class="revoke-form"
|
||||
>
|
||||
<input type="hidden" name="id" value={c.id} />
|
||||
<button type="submit" class="revoke-btn" title="Revoke">↩</button>
|
||||
</form>
|
||||
@@ -487,7 +526,7 @@
|
||||
{#each manualConfigs() as bc}
|
||||
{@const val =
|
||||
bc.rewardType === 'cash'
|
||||
? `£${(Number(bc.rewardValue) / 100).toFixed(2)}`
|
||||
? `£${Number(bc.rewardValue).toFixed(2)}`
|
||||
: bc.rewardType === 'points'
|
||||
? `${bc.rewardValue} pts`
|
||||
: bc.rewardValue}
|
||||
@@ -500,7 +539,13 @@
|
||||
<span class="trigger-value">{val}</span>
|
||||
</div>
|
||||
{#if targeted}
|
||||
<form method="POST" action="?/trigger" use:enhance={() => { return async (args: any) => handleResult(args); }}>
|
||||
<form
|
||||
method="POST"
|
||||
action="?/trigger"
|
||||
use:enhance={() => {
|
||||
return async (args: any) => handleResult(args);
|
||||
}}
|
||||
>
|
||||
<input type="hidden" name="configId" value={bc.id} />
|
||||
<input type="hidden" name="memberId" value={targeted.id} />
|
||||
<div class="trigger-row">
|
||||
@@ -511,7 +556,13 @@
|
||||
</div>
|
||||
</form>
|
||||
{:else}
|
||||
<form method="POST" action="?/trigger" use:enhance={() => { return async (args: any) => handleResult(args); }}>
|
||||
<form
|
||||
method="POST"
|
||||
action="?/trigger"
|
||||
use:enhance={() => {
|
||||
return async (args: any) => handleResult(args);
|
||||
}}
|
||||
>
|
||||
<input type="hidden" name="configId" value={bc.id} />
|
||||
<div class="trigger-row">
|
||||
<select name="memberId" class="trigger-select">
|
||||
@@ -561,6 +612,7 @@
|
||||
<div class="payment-right">
|
||||
<span class="value">{rewardLabel(r)}</span>
|
||||
<Button type="submit" size="sm" variant="primary">Issue</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{/each}
|
||||
@@ -569,13 +621,14 @@
|
||||
method="POST"
|
||||
action="?/issueAll"
|
||||
use:enhance={() => {
|
||||
return async (args: any) =>
|
||||
handleResult(args);
|
||||
return async (args: any) => handleResult(args);
|
||||
}}
|
||||
>
|
||||
<input type="hidden" name="memberId" value={m.id} />
|
||||
<input type="hidden" name="message" value={selectedMessage[m.id] || ''} />
|
||||
<Button type="submit" size="sm" variant="secondary">Issue All ({requested.length})</Button>
|
||||
<Button type="submit" size="sm" variant="secondary"
|
||||
>Issue All ({requested.length})</Button
|
||||
>
|
||||
</form>
|
||||
{/if}
|
||||
{/if}
|
||||
@@ -746,11 +799,11 @@
|
||||
</div>
|
||||
<div class="badge-card cash">
|
||||
<span class="badge-label">Total Cash</span>
|
||||
<span class="badge-value">£{(totalCash / 100).toFixed(2)}</span>
|
||||
<span class="badge-value">£{totalCash.toFixed(2)}</span>
|
||||
</div>
|
||||
<div class="badge-card pending">
|
||||
<span class="badge-label">Pending Cash</span>
|
||||
<span class="badge-value">£{(pendingCash / 100).toFixed(2)}</span>
|
||||
<span class="badge-value">£{pendingCash.toFixed(2)}</span>
|
||||
</div>
|
||||
<div class="badge-card points">
|
||||
<span class="badge-label">Total Points</span>
|
||||
@@ -1440,9 +1493,16 @@
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
.request-all-btn:hover { background: #1d4ed8; }
|
||||
.request-col { border-color: #93c5fd; background: #eff6ff; }
|
||||
.claim-card.requested { opacity: 0.7; }
|
||||
.request-all-btn:hover {
|
||||
background: #1d4ed8;
|
||||
}
|
||||
.request-col {
|
||||
border-color: #93c5fd;
|
||||
background: #eff6ff;
|
||||
}
|
||||
.claim-card.requested {
|
||||
opacity: 0.7;
|
||||
}
|
||||
.requested-badge {
|
||||
font-size: 0.7rem;
|
||||
padding: 2px 8px;
|
||||
@@ -1465,7 +1525,9 @@
|
||||
color: #2563eb;
|
||||
margin: 0 0 0.25rem;
|
||||
}
|
||||
.payment-row.outstanding { opacity: 0.6; }
|
||||
.payment-row.outstanding {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
h1 {
|
||||
display: flex;
|
||||
|
||||
@@ -4,21 +4,95 @@ import { hono } from '$lib/server/hono';
|
||||
export async function load(event) {
|
||||
if (!event.locals.session) throw redirect(303, '/login');
|
||||
const famId = event.locals.session.famId;
|
||||
const [configs, members, progress, rewards] = await Promise.all([
|
||||
const [configs, templates, members, progress, rewards] = await Promise.all([
|
||||
hono.admin.bonusConfigs(event, famId),
|
||||
hono.admin.list(event, 'bonus-templates', famId),
|
||||
hono.admin.list(event, 'members', famId),
|
||||
hono.admin.bonusConfigProgress(event, famId),
|
||||
hono.admin.rewards(event, famId),
|
||||
hono.admin.rewards(event, famId)
|
||||
]);
|
||||
return { configs, members, progress, rewards };
|
||||
return { configs, templates, members, progress, rewards };
|
||||
}
|
||||
|
||||
export const actions = {
|
||||
createTemplate: async (event) => {
|
||||
if (!event.locals.session) throw redirect(303, '/login');
|
||||
const famId = event.locals.session.famId;
|
||||
const fd = await event.request.formData();
|
||||
const data: Record<string, unknown> = {
|
||||
name: fd.get('name'),
|
||||
description: fd.get('description') || '',
|
||||
target: fd.get('target'),
|
||||
type: fd.get('type'),
|
||||
occurrence: fd.get('occurrence'),
|
||||
rewardType: fd.get('rewardType'),
|
||||
rewardValue: fd.get('rewardValue')
|
||||
};
|
||||
const criteriaValue = fd.get('criteriaValue');
|
||||
if (criteriaValue) data.criteriaValue = parseInt(criteriaValue as string, 10) || 0;
|
||||
const period = fd.get('period');
|
||||
if (period !== null) data.period = period;
|
||||
if (data.occurrence === 'once') data.period = '';
|
||||
try {
|
||||
const record = await hono.admin.create(event, 'bonus-templates', famId, data);
|
||||
return { record };
|
||||
} catch (e) {
|
||||
return fail(400, { error: e instanceof Error ? e.message : 'Failed to create template' });
|
||||
}
|
||||
},
|
||||
|
||||
updateTemplate: async (event) => {
|
||||
if (!event.locals.session) throw redirect(303, '/login');
|
||||
const famId = event.locals.session.famId;
|
||||
const fd = await event.request.formData();
|
||||
const id = fd.get('id') as string;
|
||||
const data: Record<string, unknown> = {};
|
||||
const name = fd.get('name');
|
||||
if (name) data.name = name;
|
||||
const target = fd.get('target');
|
||||
if (target) data.target = target;
|
||||
const type = fd.get('type');
|
||||
if (type) data.type = type;
|
||||
const occurrence = fd.get('occurrence');
|
||||
if (occurrence) data.occurrence = occurrence;
|
||||
const rewardType = fd.get('rewardType');
|
||||
if (rewardType) data.rewardType = rewardType;
|
||||
const rewardValue = fd.get('rewardValue');
|
||||
if (rewardValue) data.rewardValue = rewardValue;
|
||||
const criteriaValue = fd.get('criteriaValue');
|
||||
if (criteriaValue) data.criteriaValue = parseInt(criteriaValue as string, 10) || 0;
|
||||
const period = fd.get('period');
|
||||
if (period !== null) data.period = period;
|
||||
if (occurrence === 'once') data.period = '';
|
||||
const description = fd.get('description');
|
||||
if (description) data.description = description;
|
||||
try {
|
||||
const record = await hono.admin.update(event, 'bonus-templates', famId, id, data);
|
||||
return { record };
|
||||
} catch (e) {
|
||||
return fail(400, { error: e instanceof Error ? e.message : 'Failed to update template' });
|
||||
}
|
||||
},
|
||||
|
||||
deleteTemplate: async (event) => {
|
||||
if (!event.locals.session) throw redirect(303, '/login');
|
||||
const famId = event.locals.session.famId;
|
||||
const fd = await event.request.formData();
|
||||
const id = fd.get('id') as string;
|
||||
try {
|
||||
await hono.admin.remove(event, 'bonus-templates', famId, id);
|
||||
return { deleted: true };
|
||||
} catch (e) {
|
||||
return fail(400, { error: e instanceof Error ? e.message : 'Failed to delete template' });
|
||||
}
|
||||
},
|
||||
|
||||
createConfig: async (event) => {
|
||||
if (!event.locals.session) throw redirect(303, '/login');
|
||||
const famId = event.locals.session.famId;
|
||||
const fd = await event.request.formData();
|
||||
const phase = fd.get('phase') as string;
|
||||
const startMode = fd.get('startMode') as string;
|
||||
const status = startMode === 'disabled' ? 'disabled' : 'active';
|
||||
const data: Record<string, unknown> = {
|
||||
name: fd.get('name'),
|
||||
target: fd.get('target'),
|
||||
@@ -26,12 +100,13 @@ export const actions = {
|
||||
occurrence: fd.get('occurrence'),
|
||||
rewardType: fd.get('rewardType'),
|
||||
rewardValue: fd.get('rewardValue'),
|
||||
phase: phase || 'ready',
|
||||
status
|
||||
};
|
||||
const criteriaValue = fd.get('criteriaValue');
|
||||
if (criteriaValue) data.criteriaValue = parseInt(criteriaValue as string, 10) || 0;
|
||||
const period = fd.get('period');
|
||||
if (period !== null) data.period = period;
|
||||
if (data.occurrence === 'once') data.period = '';
|
||||
const description = fd.get('description');
|
||||
if (description) data.description = description;
|
||||
const memberId = fd.get('memberId');
|
||||
@@ -66,6 +141,7 @@ export const actions = {
|
||||
if (criteriaValue) data.criteriaValue = parseInt(criteriaValue as string, 10) || 0;
|
||||
const period = fd.get('period');
|
||||
if (period !== null) data.period = period;
|
||||
if (occurrence === 'once') data.period = '';
|
||||
const description = fd.get('description');
|
||||
if (description) data.description = description;
|
||||
const memberId = fd.get('memberId');
|
||||
@@ -87,7 +163,7 @@ export const actions = {
|
||||
await hono.admin.remove(event, 'bonus-configs', famId, id);
|
||||
return { deleted: true };
|
||||
} catch (e) {
|
||||
return fail(400, { error: e instanceof Error ? e.message : 'Failed to archive config' });
|
||||
return fail(400, { error: e instanceof Error ? e.message : 'Failed to delete config' });
|
||||
}
|
||||
},
|
||||
|
||||
@@ -95,6 +171,8 @@ export const actions = {
|
||||
if (!event.locals.session) throw redirect(303, '/login');
|
||||
const famId = event.locals.session.famId;
|
||||
const fd = await event.request.formData();
|
||||
const startMode = fd.get('startMode') as string;
|
||||
const status = startMode === 'disabled' ? 'disabled' : 'active';
|
||||
const data: Record<string, unknown> = {
|
||||
name: fd.get('name'),
|
||||
description: fd.get('description') || '',
|
||||
@@ -106,13 +184,16 @@ export const actions = {
|
||||
criteriaValue: parseInt(fd.get('criteriaValue') as string, 10) || 0,
|
||||
period: fd.get('period') || '',
|
||||
memberId: fd.get('memberId') || '',
|
||||
phase: 'active',
|
||||
status
|
||||
};
|
||||
if (data.occurrence === 'once') data.period = '';
|
||||
try {
|
||||
const record = await hono.admin.create(event, 'bonus-configs', famId, data);
|
||||
return { record };
|
||||
} catch (e) {
|
||||
return fail(400, { error: e instanceof Error ? e.message : 'Failed to create bonus from template' });
|
||||
return fail(400, {
|
||||
error: e instanceof Error ? e.message : 'Failed to create bonus from template'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@@ -124,7 +205,10 @@ export const actions = {
|
||||
const target = fd.get('target') as string;
|
||||
const memberId = fd.get('memberId') as string;
|
||||
try {
|
||||
const record = await hono.admin.assignBonusConfig(event, famId, id, { target, memberId: memberId || null });
|
||||
const record = await hono.admin.assignBonusConfig(event, famId, id, {
|
||||
target,
|
||||
memberId: memberId || null
|
||||
});
|
||||
return { record };
|
||||
} catch (e) {
|
||||
return fail(400, { error: e instanceof Error ? e.message : 'Failed to assign bonus' });
|
||||
@@ -136,12 +220,11 @@ export const actions = {
|
||||
const famId = event.locals.session.famId;
|
||||
const fd = await event.request.formData();
|
||||
const id = fd.get('id') as string;
|
||||
const phase = fd.get('phase') as string;
|
||||
try {
|
||||
const record = await hono.admin.completeBonusConfig(event, famId, id, phase || 'completed');
|
||||
const record = await hono.admin.completeBonusConfig(event, famId, id);
|
||||
return { record };
|
||||
} catch (e) {
|
||||
return fail(400, { error: e instanceof Error ? e.message : 'Failed to update bonus phase' });
|
||||
return fail(400, { error: e instanceof Error ? e.message : 'Failed to complete bonus' });
|
||||
}
|
||||
},
|
||||
|
||||
@@ -158,6 +241,23 @@ export const actions = {
|
||||
}
|
||||
},
|
||||
|
||||
toggleConfig: async (event) => {
|
||||
if (!event.locals.session) throw redirect(303, '/login');
|
||||
const famId = event.locals.session.famId;
|
||||
const fd = await event.request.formData();
|
||||
const id = fd.get('id') as string;
|
||||
const currentStatus = fd.get('currentStatus') as string;
|
||||
const newStatus = currentStatus === 'disabled' ? 'active' : 'disabled';
|
||||
try {
|
||||
const record = await hono.admin.update(event, 'bonus-configs', famId, id, {
|
||||
status: newStatus
|
||||
});
|
||||
return { record };
|
||||
} catch (e) {
|
||||
return fail(400, { error: e instanceof Error ? e.message : 'Failed to toggle bonus' });
|
||||
}
|
||||
},
|
||||
|
||||
claimReward: async (event) => {
|
||||
if (!event.locals.session) throw redirect(303, '/login');
|
||||
const famId = event.locals.session.famId;
|
||||
@@ -178,5 +278,5 @@ export const actions = {
|
||||
} catch (e) {
|
||||
return { error: e instanceof Error ? e.message : 'Failed to evaluate' };
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -17,7 +17,7 @@
|
||||
}
|
||||
|
||||
function rewardAmount(r: any): string {
|
||||
if (r.rewardType === 'cash') return `£${(Number(r.value) / 100).toFixed(2)}`;
|
||||
if (r.rewardType === 'cash') return `£${Number(r.value).toFixed(2)}`;
|
||||
if (r.rewardType === 'points') return `${r.value} pts`;
|
||||
return r.label;
|
||||
}
|
||||
@@ -112,7 +112,7 @@
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td class="num"><strong>Outstanding cash</strong></td>
|
||||
<td class="num"><strong>£{(totalOutstanding / 100).toFixed(2)}</strong></td>
|
||||
<td class="num"><strong>£{totalOutstanding.toFixed(2)}</strong></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
|
||||
Reference in New Issue
Block a user