ed
This commit is contained in:
parent
30bef7f1a5
commit
9e0a584816
52 changed files with 11959 additions and 0 deletions
58
resources/[tools]/ps-banking/web/src/App.svelte
Normal file
58
resources/[tools]/ps-banking/web/src/App.svelte
Normal file
|
@ -0,0 +1,58 @@
|
|||
<script lang="ts">
|
||||
import VisibilityProvider from "./providers/VisibilityProvider.svelte";
|
||||
import Main from "./components/Main.svelte";
|
||||
import { debugData } from "./utils/debugData";
|
||||
import { slide, fade } from "svelte/transition";
|
||||
import { notifications, showATM } from "../src/store/data";
|
||||
import { visibility } from "../src/store/stores";
|
||||
|
||||
debugData([
|
||||
{
|
||||
action: "openBank",
|
||||
data: true,
|
||||
},
|
||||
]);
|
||||
</script>
|
||||
|
||||
<main>
|
||||
<VisibilityProvider>
|
||||
<Main />
|
||||
</VisibilityProvider>
|
||||
{#if $showATM}
|
||||
<div
|
||||
class="absolute bottom-44 right-[22%] grid grid-cols-1 gap-2 select-none"
|
||||
>
|
||||
{#each $notifications as notification (notification.id)}
|
||||
<div
|
||||
class="bg-gray-900 text-blue-200 py-3 px-6 rounded-lg shadow-xl flex items-center space-x-3 transform transition-transform duration-500 border border-gray-700/50"
|
||||
in:slide={{ duration: 300 }}
|
||||
out:fade={{ duration: 300 }}
|
||||
>
|
||||
<i class="fa-duotone fa-{notification.icon} text-2xl"></i>
|
||||
<div>
|
||||
<p class="font-bold">{notification.title}</p>
|
||||
<p>{notification.message}</p>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<div
|
||||
class="absolute bottom-24 right-[12%] grid grid-cols-1 gap-2 select-none"
|
||||
>
|
||||
{#each $notifications as notification (notification.id)}
|
||||
<div
|
||||
class="bg-gray-900 text-blue-200 py-3 px-6 rounded-lg shadow-xl flex items-center space-x-3 transform transition-transform duration-500 border border-gray-700/50"
|
||||
in:slide={{ duration: 300 }}
|
||||
out:fade={{ duration: 300 }}
|
||||
>
|
||||
<i class="fa-duotone fa-{notification.icon} text-2xl"></i>
|
||||
<div>
|
||||
<p class="font-bold">{notification.title}</p>
|
||||
<p>{notification.message}</p>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</main>
|
3
resources/[tools]/ps-banking/web/src/app.pcss
Normal file
3
resources/[tools]/ps-banking/web/src/app.pcss
Normal file
|
@ -0,0 +1,3 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
272
resources/[tools]/ps-banking/web/src/components/ATM.svelte
Normal file
272
resources/[tools]/ps-banking/web/src/components/ATM.svelte
Normal file
|
@ -0,0 +1,272 @@
|
|||
<script lang="ts">
|
||||
import { writable, get } from "svelte/store";
|
||||
import { onMount } from "svelte";
|
||||
import { fetchNui } from "../utils/fetchNui";
|
||||
import { slide, fade, scale } from "svelte/transition";
|
||||
import { quintOut } from "svelte/easing";
|
||||
import {
|
||||
showATM,
|
||||
currentCash,
|
||||
bankBalance,
|
||||
Notify,
|
||||
type Notification,
|
||||
Locales,
|
||||
Currency,
|
||||
} from "../store/data";
|
||||
|
||||
let customWithdraw = writable(0);
|
||||
let customDeposit = writable(0);
|
||||
let withdrawAmounts = writable([]);
|
||||
let depositAmounts = writable([]);
|
||||
let gridColsPreset = writable(3);
|
||||
|
||||
$: (customDeposit = currentCash), (customWithdraw = bankBalance);
|
||||
|
||||
async function getAmountPresets() {
|
||||
try {
|
||||
const response = await fetchNui("ps-banking:client:getAmountPresets", {});
|
||||
const amounts = JSON.parse(response);
|
||||
withdrawAmounts.set(amounts.withdrawAmounts);
|
||||
depositAmounts.set(amounts.depositAmounts);
|
||||
gridColsPreset.set(amounts.grid);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
async function updateBalances() {
|
||||
try {
|
||||
const response = await fetchNui("ps-banking:client:getMoneyTypes", {});
|
||||
const bank = response.find(
|
||||
(item: { name: string }) => item.name === "bank"
|
||||
);
|
||||
const cash = response.find(
|
||||
(item: { name: string }) => item.name === "cash"
|
||||
);
|
||||
if (bank) {
|
||||
bankBalance.set(bank.amount);
|
||||
}
|
||||
if (cash) {
|
||||
currentCash.set(cash.amount);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
async function heav(amount: number) {
|
||||
try {
|
||||
const response = await fetchNui("ps-banking:client:ATMwithdraw", {
|
||||
amount: amount,
|
||||
});
|
||||
if (response) {
|
||||
Notify(
|
||||
`${$Locales.withdrawn} ${amount.toLocaleString($Currency.lang, {
|
||||
style: "currency",
|
||||
currency: $Currency.currency,
|
||||
minimumFractionDigits: 0,
|
||||
})}`,
|
||||
$Locales.payment_completed,
|
||||
"coins"
|
||||
);
|
||||
await updateBalances();
|
||||
} else {
|
||||
Notify($Locales.no_money_on_account, $Locales.error, "credit-card");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
async function deposit(amount: number) {
|
||||
try {
|
||||
const response = await fetchNui("ps-banking:client:ATMdeposit", {
|
||||
amount: amount,
|
||||
});
|
||||
if (response) {
|
||||
Notify(
|
||||
`${$Locales.deposited} ${amount.toLocaleString($Currency.lang, {
|
||||
style: "currency",
|
||||
currency: $Currency.currency,
|
||||
minimumFractionDigits: 0,
|
||||
})}`,
|
||||
$Locales.payment_completed,
|
||||
"coins"
|
||||
);
|
||||
await updateBalances();
|
||||
} else {
|
||||
Notify($Locales.no_cash_on_you, $Locales.error, "credit-card");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
async function getLocales() {
|
||||
try {
|
||||
const response = await fetchNui("ps-banking:client:getLocales", {});
|
||||
Locales.set(response);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
getAmountPresets();
|
||||
getLocales();
|
||||
updateBalances();
|
||||
const keyHandler = (e: KeyboardEvent) => {
|
||||
if (get(showATM) && ["Escape"].includes(e.code)) {
|
||||
fetchNui("ps-banking:client:hideUI");
|
||||
showATM.set(false);
|
||||
}
|
||||
};
|
||||
window.addEventListener("keydown", keyHandler);
|
||||
return () => window.removeEventListener("keydown", keyHandler);
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="absolute w-screen h-screen flex items-center justify-center">
|
||||
<div
|
||||
class="absolute inset-0 flex items-center justify-center"
|
||||
in:scale={{ duration: 1000, easing: quintOut }}
|
||||
out:scale={{ duration: 1000, easing: quintOut }}
|
||||
>
|
||||
<div
|
||||
class="h-auto w-[60%] bg-gray-800 rounded-3xl p-8 shadow-2xl relative border border-blue-200/10"
|
||||
>
|
||||
<div class="text-4xl font-bold text-center text-blue-200 mb-6">
|
||||
<i class="fa-duotone fa-atm text-blue-200 mr-2"></i>{$Locales.atm}
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-6 mb-8">
|
||||
<div
|
||||
class="bg-gray-700 p-6 rounded-2xl shadow-lg flex flex-col items-center justify-center"
|
||||
>
|
||||
<div class="text-2xl font-semibold text-blue-100 mb-2">
|
||||
<i class="fa-duotone fa-money-bill-wave text-md mr-2"></i>
|
||||
{$Locales.cash}
|
||||
</div>
|
||||
<div class="text-4xl font-bold text-blue-400">
|
||||
{$currentCash.toLocaleString($Currency.lang, {
|
||||
style: "currency",
|
||||
currency: $Currency.currency,
|
||||
minimumFractionDigits: 0,
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="bg-gray-700 p-6 rounded-2xl shadow-lg flex flex-col items-center justify-center"
|
||||
>
|
||||
<div class="text-2xl font-semibold text-blue-100 mb-2">
|
||||
<i class="fa-duotone fa-vault text-md mr-2"
|
||||
></i>{$Locales.bank_balance}
|
||||
</div>
|
||||
<div class="text-4xl font-bold text-blue-400">
|
||||
{$bankBalance.toLocaleString($Currency.lang, {
|
||||
style: "currency",
|
||||
currency: $Currency.currency,
|
||||
minimumFractionDigits: 0,
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid" style={`grid-template-columns: repeat(${$gridColsPreset}, minmax(0, 1fr)); gap: 10px;`}>
|
||||
{#each $withdrawAmounts as amount}
|
||||
<button
|
||||
class="bg-blue-600/10 border border-blue-500 hover:bg-blue-800/50 text-white font-bold py-4 px-6 rounded-xl duration-500 cursor-pointer flex items-center justify-center gap-2"
|
||||
on:click={() => {
|
||||
heav(amount);
|
||||
}}
|
||||
>
|
||||
<i class="fa-duotone fa-money-bill-wave text-lg"
|
||||
></i>{$Locales.withdraw}
|
||||
{amount.toLocaleString($Currency.lang, {
|
||||
style: "currency",
|
||||
currency: $Currency.currency,
|
||||
minimumFractionDigits: 0,
|
||||
})}
|
||||
</button>
|
||||
{/each}
|
||||
{#each $depositAmounts as amount}
|
||||
<button
|
||||
class="bg-green-600/10 border border-green-500 hover:bg-green-800/50 text-white font-bold py-4 px-6 rounded-xl duration-500 cursor-pointer flex items-center justify-center gap-2"
|
||||
on:click={() => {
|
||||
deposit(amount);
|
||||
}}
|
||||
>
|
||||
<i class="fa-duotone fa-credit-card text-lg"></i>{$Locales.deposit}
|
||||
{amount.toLocaleString($Currency.lang, {
|
||||
style: "currency",
|
||||
currency: $Currency.currency,
|
||||
minimumFractionDigits: 0,
|
||||
})}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4 mt-4">
|
||||
<div class="bg-gray-700 p-4 rounded-xl shadow-lg">
|
||||
<div class="flex items-center mb-2">
|
||||
<i
|
||||
class="fa-duotone fa-money-check-edit text-xl text-green-400 mr-2"
|
||||
></i>
|
||||
<label for="Deposit" class="text-lg font-semibold text-white">
|
||||
{$Locales.deposit_amount}
|
||||
</label>
|
||||
</div>
|
||||
<input
|
||||
type="number"
|
||||
id="Deposit"
|
||||
bind:value={$customDeposit}
|
||||
class="w-full bg-gray-800 text-white font-bold pl-4 pr-4 py-3 rounded-lg border border-green-200/10 focus:outline-none focus:border-green-400/50 transition-colors duration-500 placeholder-gray-500"
|
||||
placeholder={$Locales.deposit_amount}
|
||||
/>
|
||||
<button
|
||||
class="mt-2 w-full bg-green-600/10 border border-green-500 hover:bg-green-800/50 text-white font-bold py-2 px-4 rounded-xl duration-500 cursor-pointer flex items-center justify-center gap-2"
|
||||
on:click={() => {
|
||||
deposit(get(customDeposit));
|
||||
}}
|
||||
>
|
||||
<i class="fa-duotone fa-piggy-bank text-lg"></i>
|
||||
{$Locales.submit}
|
||||
</button>
|
||||
</div>
|
||||
<div class="bg-gray-700 p-4 rounded-xl shadow-lg">
|
||||
<div class="flex items-center mb-2">
|
||||
<i class="fa-duotone fa-money-check-edit text-xl text-blue-400 mr-2"
|
||||
></i>
|
||||
<label for="Withdraw" class="text-lg font-semibold text-white">
|
||||
{$Locales.withdraw_amount}
|
||||
</label>
|
||||
</div>
|
||||
<input
|
||||
type="number"
|
||||
id="Withdraw"
|
||||
bind:value={$customWithdraw}
|
||||
class="w-full bg-gray-800 font-bold text-white pl-4 pr-4 py-3 rounded-lg border border-blue-200/10 focus:outline-none focus:border-blue-400/50 transition-colors duration-500 placeholder-gray-500"
|
||||
placeholder={$Locales.withdraw_amount}
|
||||
/>
|
||||
<button
|
||||
class="mt-2 w-full bg-blue-600/10 border border-blue-500 hover:bg-blue-800/50 text-white font-bold py-2 px-4 rounded-xl duration-500 cursor-pointer flex items-center justify-center gap-2"
|
||||
on:click={() => {
|
||||
heav(get(customWithdraw));
|
||||
}}
|
||||
>
|
||||
<i class="fa-duotone fa-money-bill-wave text-lg"></i>
|
||||
{$Locales.submit}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="absolute top-4 right-4 transform -translate-x-1/2 text-white">
|
||||
<button
|
||||
class="text-blue-200/50 hover:text-blue-500/50 font-bold py-2 px-4 rounded-xl duration-500 cursor-pointer flex items-center justify-center gap-2 text-gray-300 py-4 transition-all duration-500 rounded-xl hover:text-blue-300 duration-500 hover:cursor-pointer hover:bg-gray-800/80"
|
||||
on:click={() => {
|
||||
showATM.set(false);
|
||||
fetchNui("ps-banking:client:hideUI");
|
||||
}}
|
||||
>
|
||||
<i class="fa-duotone fa-times-circle text-2xl"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
801
resources/[tools]/ps-banking/web/src/components/Accounts.svelte
Normal file
801
resources/[tools]/ps-banking/web/src/components/Accounts.svelte
Normal file
|
@ -0,0 +1,801 @@
|
|||
<script lang="ts">
|
||||
import { writable, get, derived } from "svelte/store";
|
||||
import { slide, scale, fade } from "svelte/transition";
|
||||
import { quintOut } from "svelte/easing";
|
||||
import { Notify, type Notification, Locales, Currency } from "../store/data";
|
||||
import { fetchNui } from "../utils/fetchNui";
|
||||
import { onMount } from "svelte";
|
||||
let userData = writable({});
|
||||
let accounts = writable([]);
|
||||
let totalBalance = derived(accounts, ($accounts) =>
|
||||
$accounts.reduce((acc, account) => acc + account.balance, 0)
|
||||
);
|
||||
let showModal = writable(false);
|
||||
let showRenameModal = writable(false);
|
||||
let showCreateAccountModal = writable(false);
|
||||
let showDeleteModal = writable(false);
|
||||
let showRemoveUserModal = writable(false);
|
||||
let showWithdrawModal = writable(false);
|
||||
let showDepositModal = writable(false);
|
||||
let newServerId = writable("");
|
||||
let newAccountName = writable("");
|
||||
let newAccountHolder = writable("");
|
||||
let newAccountBalance = writable(0);
|
||||
let selectedAccount = writable<number | null>(null);
|
||||
let selectedUser = writable("");
|
||||
let transactionAmount = writable<number>(0);
|
||||
|
||||
async function renameAccount(id: number) {
|
||||
const newName = get(newAccountName);
|
||||
if (newName) {
|
||||
try {
|
||||
const response = await fetchNui("ps-banking:client:renameAccount", {
|
||||
id,
|
||||
newName,
|
||||
});
|
||||
if (response.success) {
|
||||
accounts.update((accs) =>
|
||||
accs.map((acc) =>
|
||||
acc.id === id ? { ...acc, holder: newName } : acc
|
||||
)
|
||||
);
|
||||
newAccountName.set("");
|
||||
showRenameModal.set(false);
|
||||
getAccounts();
|
||||
Notify(
|
||||
$Locales.account_renamed_successfully,
|
||||
$Locales.success,
|
||||
"check-circle"
|
||||
);
|
||||
} else {
|
||||
Notify(
|
||||
$Locales.account_rename_failed,
|
||||
$Locales.error,
|
||||
"exclamation-circle"
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
Notify(
|
||||
$Locales.account_rename_failed,
|
||||
$Locales.error,
|
||||
"exclamation-circle"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function copyAccountNumber(id: number) {
|
||||
const account = get(accounts).find((acc) => acc.id === id);
|
||||
if (account) {
|
||||
await fetchNui("ps-banking:client:copyAccountNumber", {
|
||||
accountNumber: account.cardNumber,
|
||||
});
|
||||
Notify($Locales.account_number_copied, $Locales.success, "clipboard");
|
||||
}
|
||||
}
|
||||
|
||||
async function addUserToAccount(accountId: number, userId: string) {
|
||||
try {
|
||||
const response = await fetchNui("ps-banking:client:addUserToAccount", {
|
||||
accountId,
|
||||
userId,
|
||||
});
|
||||
if (response.success) {
|
||||
accounts.update((accs) => {
|
||||
const updatedAccounts = accs.map((acc) => {
|
||||
if (acc.id === accountId) {
|
||||
return {
|
||||
...acc,
|
||||
users: [
|
||||
...acc.users,
|
||||
{ name: response.userName, identifier: userId },
|
||||
],
|
||||
};
|
||||
}
|
||||
return acc;
|
||||
});
|
||||
return updatedAccounts;
|
||||
});
|
||||
Notify(
|
||||
`${response.userName} ${$Locales.user_added_successfully}`,
|
||||
$Locales.success,
|
||||
"check-circle"
|
||||
);
|
||||
showModal.set(false);
|
||||
newServerId.set("0");
|
||||
getAccounts();
|
||||
} else {
|
||||
Notify(response.message, $Locales.error, "exclamation-circle");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
Notify(
|
||||
$Locales.user_addition_failed,
|
||||
$Locales.error,
|
||||
"exclamation-circle"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function removeUserFromAccount() {
|
||||
const accountId = get(selectedAccount);
|
||||
const user = get(selectedUser);
|
||||
if (accountId !== null && user) {
|
||||
try {
|
||||
const response = await fetchNui(
|
||||
"ps-banking:client:removeUserFromAccount",
|
||||
{ accountId, user }
|
||||
);
|
||||
if (response.success) {
|
||||
accounts.update((accs) => {
|
||||
const updatedAccounts = accs.map((acc) =>
|
||||
acc.id === accountId
|
||||
? {
|
||||
...acc,
|
||||
users: acc.users.filter((u) => u.identifier !== user),
|
||||
}
|
||||
: acc
|
||||
);
|
||||
return updatedAccounts;
|
||||
});
|
||||
Notify(
|
||||
`${$Locales.removed_successfully}`,
|
||||
$Locales.success,
|
||||
"check-circle"
|
||||
);
|
||||
selectedUser.set("");
|
||||
showRemoveUserModal.set(false);
|
||||
getAccounts();
|
||||
} else {
|
||||
Notify(
|
||||
$Locales.user_removal_failed,
|
||||
$Locales.error,
|
||||
"exclamation-circle"
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
Notify(
|
||||
$Locales.user_removal_failed,
|
||||
$Locales.error,
|
||||
"exclamation-circle"
|
||||
);
|
||||
}
|
||||
} else {
|
||||
Notify(
|
||||
$Locales.select_account_and_user,
|
||||
$Locales.error,
|
||||
"exclamation-circle"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteAccount(accountId: number) {
|
||||
const response = await fetchNui("ps-banking:client:deleteAccount", {
|
||||
accountId,
|
||||
});
|
||||
if (response.success) {
|
||||
accounts.update((accs) => accs.filter((acc) => acc.id !== accountId));
|
||||
Notify(
|
||||
$Locales.account_deleted_successfully,
|
||||
$Locales.success,
|
||||
"check-circle"
|
||||
);
|
||||
showDeleteModal.set(false);
|
||||
} else {
|
||||
Notify(
|
||||
$Locales.account_deletion_failed,
|
||||
$Locales.error,
|
||||
"exclamation-circle"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function formatCardNumber(cardNumber: string): string {
|
||||
return cardNumber.match(/.{1,4}/g)?.join(" ") || cardNumber;
|
||||
}
|
||||
|
||||
async function createNewAccount() {
|
||||
const holder = get(newAccountHolder);
|
||||
const balance = get(newAccountBalance);
|
||||
const newId = Math.max(...get(accounts).map((acc) => acc.id)) + 1;
|
||||
const rawCardNumber = Math.random().toString().slice(2, 18);
|
||||
const cardNumber = formatCardNumber(rawCardNumber);
|
||||
const newAccount = {
|
||||
id: newId,
|
||||
balance: balance,
|
||||
holder: holder,
|
||||
cardNumber: cardNumber,
|
||||
users: [],
|
||||
owner: {
|
||||
state: true,
|
||||
name: get(userData).name,
|
||||
identifier: get(userData).identifier,
|
||||
},
|
||||
};
|
||||
const response = await fetchNui("ps-banking:client:createNewAccount", {
|
||||
newAccount,
|
||||
});
|
||||
if (response.success) {
|
||||
accounts.update((accs) => [...accs, newAccount]);
|
||||
newAccountHolder.set("");
|
||||
newAccountBalance.set(0);
|
||||
showCreateAccountModal.set(false);
|
||||
getAccounts();
|
||||
Notify(
|
||||
$Locales.new_account_created_successfully,
|
||||
$Locales.success,
|
||||
"check-circle"
|
||||
);
|
||||
} else {
|
||||
Notify(
|
||||
$Locales.new_account_creation_failed,
|
||||
$Locales.error,
|
||||
"exclamation-circle"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function withdrawFromAccount() {
|
||||
const accountId = get(selectedAccount);
|
||||
const amount = get(transactionAmount);
|
||||
if (accountId !== null && amount > 0) {
|
||||
const response = await fetchNui("ps-banking:client:withdrawFromAccount", {
|
||||
accountId,
|
||||
amount,
|
||||
});
|
||||
if (response.success) {
|
||||
accounts.update((accs) => {
|
||||
const updatedAccounts = accs.map((acc) =>
|
||||
acc.id === accountId && acc.balance >= amount
|
||||
? { ...acc, balance: acc.balance - amount }
|
||||
: acc
|
||||
);
|
||||
return updatedAccounts;
|
||||
});
|
||||
Notify(
|
||||
`${$Locales.withdrew} ${amount} ${$Locales.successfully}`,
|
||||
$Locales.success,
|
||||
"check-circle"
|
||||
);
|
||||
transactionAmount.set(0);
|
||||
showWithdrawModal.set(false);
|
||||
} else {
|
||||
Notify(
|
||||
$Locales.withdrawal_failed,
|
||||
$Locales.error,
|
||||
"exclamation-circle"
|
||||
);
|
||||
}
|
||||
} else {
|
||||
Notify(
|
||||
$Locales.select_valid_account_and_amount,
|
||||
$Locales.error,
|
||||
"exclamation-circle"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function depositToAccount() {
|
||||
const accountId = get(selectedAccount);
|
||||
const amount = get(transactionAmount);
|
||||
if (accountId !== null && amount > 0) {
|
||||
const response = await fetchNui("ps-banking:client:depositToAccount", {
|
||||
accountId,
|
||||
amount,
|
||||
});
|
||||
if (response.success) {
|
||||
accounts.update((accs) => {
|
||||
const updatedAccounts = accs.map((acc) =>
|
||||
acc.id === accountId
|
||||
? { ...acc, balance: acc.balance + amount }
|
||||
: acc
|
||||
);
|
||||
return updatedAccounts;
|
||||
});
|
||||
Notify(
|
||||
`${$Locales.deposited} ${amount} ${$Locales.successfully}`,
|
||||
$Locales.success,
|
||||
"check-circle"
|
||||
);
|
||||
transactionAmount.set(0);
|
||||
showDepositModal.set(false);
|
||||
} else {
|
||||
Notify($Locales.deposit_failed, $Locales.error, "exclamation-circle");
|
||||
}
|
||||
} else {
|
||||
Notify(
|
||||
$Locales.select_valid_account_and_amount,
|
||||
$Locales.error,
|
||||
"exclamation-circle"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function getUser() {
|
||||
try {
|
||||
const response = await fetchNui("ps-banking:client:getUser", {});
|
||||
userData.set(response);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
async function getAccounts() {
|
||||
try {
|
||||
const response = await fetchNui("ps-banking:client:getAccounts", {});
|
||||
accounts.set(response);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
getUser();
|
||||
getAccounts();
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="absolute w-full h-full bg-gray-800 text-white">
|
||||
<div
|
||||
class="absolute w-[90%] h-full p-6 overflow-auto left-[130px]"
|
||||
in:slide={{ duration: 1000, easing: quintOut }}
|
||||
>
|
||||
<div
|
||||
class="bg-gray-700/10 p-8 rounded-xl shadow-lg border border-blue-200/5"
|
||||
>
|
||||
<div class="text-5xl font-extrabold text-left text-blue-400 mb-10">
|
||||
<i class="fa-duotone fa-users text-3xl text-blue-200 mr-3"></i>
|
||||
{$Locales.accounts}
|
||||
</div>
|
||||
<div class="relative grid grid-cols-3 gap-y-10 gap-x-4 w-[90%]">
|
||||
{#each $accounts as account (account.id)}
|
||||
<div
|
||||
class="py-8 px-8 w-auto h-auto rounded-2xl shadow-lg flex flex-col justify-between bg-[#1c2333] text-blue-400 relative"
|
||||
out:fade={{ duration: 1000, easing: quintOut }}
|
||||
>
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="text-2xl font-bold">
|
||||
{account.balance.toLocaleString($Currency.lang, {
|
||||
style: "currency",
|
||||
currency: $Currency.currency,
|
||||
minimumFractionDigits: 0,
|
||||
})}
|
||||
</div>
|
||||
<div class="text-sm font-semibold">#{account.id}</div>
|
||||
</div>
|
||||
<div class="text-xl mt-2">{account.cardNumber}</div>
|
||||
<div class="flex justify-between items-center mt-4">
|
||||
<div class="text-lg">{account.owner.name} - {account.holder}</div>
|
||||
<div class="flex space-x-2">
|
||||
<button
|
||||
class="text-gray-400 hover:text-blue-300 duration-500"
|
||||
on:click={() => copyAccountNumber(account.id)}
|
||||
>
|
||||
<i class="fa-duotone fa-copy"></i>
|
||||
</button>
|
||||
{#if account.owner.identifier === get(userData).identifier || account.users.some(user => user.identifier === get(userData).identifier)}
|
||||
<button
|
||||
class="text-gray-400 hover:text-blue-300 duration-500"
|
||||
on:click={() => showRenameModal.set(account.id)}
|
||||
>
|
||||
<i class="fa-duotone fa-pen"></i>
|
||||
</button>
|
||||
{#if account.owner.identifier === get(userData).identifier}
|
||||
<button
|
||||
class="text-gray-400 hover:text-blue-300 duration-500"
|
||||
on:click={() => showModal.set(account.id)}
|
||||
>
|
||||
<i class="fa-duotone fa-user-plus"></i>
|
||||
</button>
|
||||
<button
|
||||
class="text-gray-400 hover:text-blue-300 duration-500"
|
||||
on:click={() => {
|
||||
selectedAccount.set(account.id);
|
||||
selectedUser.set("");
|
||||
showRemoveUserModal.set(true);
|
||||
}}
|
||||
>
|
||||
<i class="fa-duotone fa-user-minus"></i>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="text-red-400 hover:text-red-300 duration-500"
|
||||
on:click={() => {
|
||||
selectedAccount.set(account.id);
|
||||
showDeleteModal.set(true);
|
||||
}}
|
||||
>
|
||||
<i class="fa-duotone fa-trash"></i>
|
||||
</button>
|
||||
{/if}
|
||||
<button
|
||||
class="text-green-400 hover:text-green-300 duration-500"
|
||||
on:click={() => {
|
||||
selectedAccount.set(account.id);
|
||||
showDepositModal.set(true);
|
||||
}}
|
||||
>
|
||||
<i class="fa-duotone fa-arrow-up"></i>
|
||||
</button>
|
||||
<button
|
||||
class="text-yellow-400 hover:text-yellow-300 duration-500"
|
||||
on:click={() => {
|
||||
selectedAccount.set(account.id);
|
||||
showWithdrawModal.set(true);
|
||||
}}
|
||||
>
|
||||
<i class="fa-duotone fa-arrow-down"></i>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
<button
|
||||
class="bg-[#1c2333] mt-6 py-8 px-8 w-[250px] h-[200px] rounded-2xl shadow-lg flex items-center justify-center cursor-pointer border border-dashed border-blue-400 hover:border-blue-600 transition-all duration-500"
|
||||
on:click={() => showCreateAccountModal.set(true)}
|
||||
>
|
||||
<i class="fa-duotone fa-plus text-4xl text-gray-200"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if $showModal}
|
||||
<div
|
||||
class="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50 z-50"
|
||||
>
|
||||
<div
|
||||
class="bg-[#1c2333] p-8 rounded-lg shadow-2xl w-96 relative"
|
||||
in:scale={{ duration: 250, easing: quintOut }}
|
||||
out:scale={{ duration: 250, easing: quintOut }}
|
||||
>
|
||||
<h2 class="text-2xl font-bold text-blue-400 mb-4 flex items-center">
|
||||
<i class="fa-duotone fa-exchange-alt mr-2"></i>
|
||||
{$Locales.new_user_to_account}
|
||||
</h2>
|
||||
<div class="mb-4">
|
||||
<label class="block text-blue-400 mb-2" for="ServerID"
|
||||
>{$Locales.server_id}</label
|
||||
>
|
||||
<div class="relative">
|
||||
<input
|
||||
type="number"
|
||||
id="ServerID"
|
||||
bind:value={$newServerId}
|
||||
class="w-full bg-[#283040] text-white font-bold pl-4 pr-12 py-3 rounded-lg border border-blue-400 focus:outline-none focus:border-blue-600 transition-colors duration-500 placeholder-gray-500"
|
||||
placeholder="ID"
|
||||
/>
|
||||
<i
|
||||
class="fa-duotone fa-id-card absolute top-1/2 right-4 transform -translate-y-1/2 text-blue-400"
|
||||
></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-center space-x-4">
|
||||
<button
|
||||
class="bg-red-600 text-white py-2 px-4 rounded-lg flex items-center transition-colors duration-300"
|
||||
on:click={() => {
|
||||
showModal.set(false);
|
||||
newServerId.set(0);
|
||||
}}
|
||||
>
|
||||
<i class="fa-duotone fa-times-circle text-lg mr-2"></i>
|
||||
{$Locales.cancel}
|
||||
</button>
|
||||
<button
|
||||
class="bg-blue-600 text-white py-2 px-4 rounded-lg flex items-center transition-colors duration-300"
|
||||
on:click={() => addUserToAccount(get(showModal), $newServerId)}
|
||||
>
|
||||
<i class="fa-duotone fa-check-circle text-lg mr-2"></i>
|
||||
{$Locales.add_user}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if $showRenameModal}
|
||||
<div
|
||||
class="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50 z-50"
|
||||
>
|
||||
<div
|
||||
class="bg-[#1c2333] p-8 rounded-lg shadow-2xl w-96 relative"
|
||||
in:scale={{ duration: 250, easing: quintOut }}
|
||||
out:scale={{ duration: 250, easing: quintOut }}
|
||||
>
|
||||
<h2 class="text-2xl font-bold text-blue-400 mb-4 flex items-center">
|
||||
<i class="fa-duotone fa-edit mr-2"></i>
|
||||
{$Locales.rename_account}
|
||||
</h2>
|
||||
<div class="mb-4">
|
||||
<label class="block text-blue-400 mb-2" for="AccountName"
|
||||
>{$Locales.new_account_name}</label
|
||||
>
|
||||
<div class="relative">
|
||||
<input
|
||||
type="text"
|
||||
id="AccountName"
|
||||
bind:value={$newAccountName}
|
||||
class="w-full bg-[#283040] text-white font-bold pl-4 pr-12 py-3 rounded-lg border border-blue-400 focus:outline-none focus:border-blue-600 transition-colors duration-500 placeholder-gray-500"
|
||||
placeholder={$Locales.new_name}
|
||||
/>
|
||||
<i
|
||||
class="fa-duotone fa-pen-nib absolute top-1/2 right-4 transform -translate-y-1/2 text-blue-400"
|
||||
></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-center space-x-4">
|
||||
<button
|
||||
class="bg-red-600 text-white py-2 px-4 rounded-lg flex items-center transition-colors duration-300"
|
||||
on:click={() => showRenameModal.set(false)}
|
||||
>
|
||||
<i class="fa-duotone fa-times-circle text-lg mr-2"></i>
|
||||
{$Locales.cancel}
|
||||
</button>
|
||||
<button
|
||||
class="bg-blue-600 text-white py-2 px-4 rounded-lg flex items-center transition-colors duration-300"
|
||||
on:click={() => renameAccount(get(showRenameModal))}
|
||||
>
|
||||
<i class="fa-duotone fa-check-circle text-lg mr-2"></i>
|
||||
{$Locales.rename}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if $showCreateAccountModal}
|
||||
<div
|
||||
class="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50 z-50"
|
||||
>
|
||||
<div
|
||||
class="bg-[#1c2333] p-8 rounded-lg shadow-2xl w-96 relative"
|
||||
in:scale={{ duration: 250, easing: quintOut }}
|
||||
out:scale={{ duration: 250, easing: quintOut }}
|
||||
>
|
||||
<h2 class="text-2xl font-bold text-blue-400 mb-4 flex items-center">
|
||||
<i class="fa-duotone fa-plus mr-2"></i>
|
||||
{$Locales.create_new_account}
|
||||
</h2>
|
||||
<div class="mb-4">
|
||||
<label class="block text-blue-400 mb-2" for="AccountHolder"
|
||||
>{$Locales.account_holder}</label
|
||||
>
|
||||
<div class="relative">
|
||||
<input
|
||||
type="text"
|
||||
id="AccountHolder"
|
||||
bind:value={$newAccountHolder}
|
||||
class="w-full bg-[#283040] text-white font-bold pl-4 pr-12 py-3 rounded-lg border border-blue-400 focus:outline-none focus:border-blue-600 transition-colors duration-500 placeholder-gray-500"
|
||||
placeholder={$Locales.account_holder}
|
||||
/>
|
||||
<i
|
||||
class="fa-duotone fa-user absolute top-1/2 right-4 transform -translate-y-1/2 text-blue-400"
|
||||
></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-center space-x-4">
|
||||
<button
|
||||
class="bg-red-600 text-white py-2 px-4 rounded-lg flex items-center transition-colors duration-300"
|
||||
on:click={() => showCreateAccountModal.set(false)}
|
||||
>
|
||||
<i class="fa-duotone fa-times-circle text-lg mr-2"></i>
|
||||
{$Locales.cancel}
|
||||
</button>
|
||||
<button
|
||||
class="bg-blue-600 text-white py-2 px-4 rounded-lg flex items-center transition-colors duration-300"
|
||||
on:click={createNewAccount}
|
||||
>
|
||||
<i class="fa-duotone fa-check-circle text-lg mr-2"></i>
|
||||
{$Locales.create}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if $showDeleteModal}
|
||||
<div
|
||||
class="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50 z-50"
|
||||
>
|
||||
<div
|
||||
class="bg-[#1c2333] p-8 rounded-lg shadow-2xl w-96 relative"
|
||||
in:scale={{ duration: 250, easing: quintOut }}
|
||||
out:scale={{ duration: 250, easing: quintOut }}
|
||||
>
|
||||
<h2 class="text-2xl font-bold text-red-400 mb-4 flex items-center">
|
||||
<i class="fa-duotone fa-exclamation-triangle mr-2"></i>
|
||||
{$Locales.delete_account}
|
||||
</h2>
|
||||
<p class="text-blue-400 mb-4">
|
||||
{$Locales.are_you_sure_you_want_to_delete_this_account}
|
||||
</p>
|
||||
<div class="flex justify-center space-x-4">
|
||||
<button
|
||||
class="bg-gray-600 text-white py-2 px-4 rounded-lg flex items-center transition-colors duration-300"
|
||||
on:click={() => showDeleteModal.set(false)}
|
||||
>
|
||||
<i class="fa-duotone fa-times-circle text-lg mr-2"></i>
|
||||
{$Locales.cancel}
|
||||
</button>
|
||||
<button
|
||||
class="bg-red-600 text-white py-2 px-4 rounded-lg flex items-center transition-colors duration-300"
|
||||
on:click={() => deleteAccount(get(selectedAccount))}
|
||||
>
|
||||
<i class="fa-duotone fa-check-circle text-lg mr-2"></i>
|
||||
{$Locales.delete}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if $showRemoveUserModal}
|
||||
<div
|
||||
class="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50 z-50"
|
||||
>
|
||||
<div
|
||||
class="bg-[#1c2333] p-8 rounded-lg shadow-2xl w-96 relative"
|
||||
in:scale={{ duration: 250, easing: quintOut }}
|
||||
out:scale={{ duration: 250, easing: quintOut }}
|
||||
>
|
||||
<h2 class="text-2xl font-bold text-blue-400 mb-4 flex items-center">
|
||||
<i class="fa-duotone fa-user-minus mr-2"></i>
|
||||
{$Locales.remove_user_from_account}
|
||||
</h2>
|
||||
<div class="mb-4">
|
||||
<label class="block text-blue-400 mb-2" for="UserSelect"
|
||||
>{$Locales.select_user}</label
|
||||
>
|
||||
<div class="relative">
|
||||
<select
|
||||
id="UserSelect"
|
||||
bind:value={$selectedUser}
|
||||
class="w-full bg-[#283040] text-white font-bold pl-4 pr-12 py-3 rounded-lg border border-blue-400 focus:outline-none focus:border-blue-600 transition-colors duration-500 placeholder-gray-500 appearance-none"
|
||||
style="background-image: none; -moz-appearance: none; -webkit-appearance: none;"
|
||||
>
|
||||
{#each $accounts.find((acc) => acc.id === $selectedAccount)?.users as user}
|
||||
<option
|
||||
value={user.identifier}
|
||||
class="bg-[#283040] text-white rounded-xl font-bold pl-4 pr-12 py-4 rounded-lg transition-colors duration-500 hover:bg-blue-300/20 hover:text-gray-200 border-b border-blue-200"
|
||||
>
|
||||
{user.name}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
<i
|
||||
class="fa-duotone fa-user absolute top-1/2 right-4 transform -translate-y-1/2 text-blue-400"
|
||||
></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-center space-x-4">
|
||||
<button
|
||||
class="bg-gray-600 text-white py-2 px-4 rounded-lg flex items-center transition-colors duration-300"
|
||||
on:click={() => {
|
||||
showRemoveUserModal.set(false);
|
||||
selectedUser.set("");
|
||||
}}
|
||||
>
|
||||
<i class="fa-duotone fa-times-circle text-lg mr-2"></i>
|
||||
{$Locales.cancel}
|
||||
</button>
|
||||
<button
|
||||
class="bg-red-600 text-white py-2 px-4 rounded-lg flex items-center transition-colors duration-300"
|
||||
on:click={removeUserFromAccount}
|
||||
>
|
||||
<i class="fa-duotone fa-check-circle text-lg mr-2"></i>
|
||||
{$Locales.remove}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if $showWithdrawModal}
|
||||
<div
|
||||
class="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50 z-50"
|
||||
>
|
||||
<div
|
||||
class="bg-[#1c2333] p-8 rounded-lg shadow-2xl w-96 relative"
|
||||
in:scale={{ duration: 250, easing: quintOut }}
|
||||
out:scale={{ duration: 250, easing: quintOut }}
|
||||
>
|
||||
<h2 class="text-2xl font-bold text-yellow-400 mb-4 flex items-center">
|
||||
<i class="fa-duotone fa-arrow-down mr-2"></i>
|
||||
{$Locales.withdraw_from_account}
|
||||
</h2>
|
||||
<div class="mb-4">
|
||||
<label class="block text-yellow-400 mb-2" for="WithdrawAmount"
|
||||
>{$Locales.withdraw_amount}</label
|
||||
>
|
||||
<div class="relative">
|
||||
<input
|
||||
type="number"
|
||||
id="WithdrawAmount"
|
||||
bind:value={$transactionAmount}
|
||||
class="w-full bg-[#283040] text-white font-bold pl-4 pr-12 py-3 rounded-lg border border-yellow-400 focus:outline-none focus:border-yellow-600 transition-colors duration-500 placeholder-gray-500"
|
||||
placeholder="0"
|
||||
/>
|
||||
<i
|
||||
class="fa-duotone fa-dollar-sign absolute top-1/2 right-4 transform -translate-y-1/2 text-yellow-400"
|
||||
></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-center space-x-4">
|
||||
<button
|
||||
class="bg-gray-600 text-white py-2 px-4 rounded-lg flex items-center transition-colors duration-300"
|
||||
on:click={() => {
|
||||
showWithdrawModal.set(false);
|
||||
transactionAmount.set(0);
|
||||
}}
|
||||
>
|
||||
<i class="fa-duotone fa-times-circle text-lg mr-2"></i>
|
||||
{$Locales.cancel}
|
||||
</button>
|
||||
<button
|
||||
class="bg-yellow-600 text-white py-2 px-4 rounded-lg flex items-center transition-colors duration-300"
|
||||
on:click={withdrawFromAccount}
|
||||
>
|
||||
<i class="fa-duotone fa-check-circle text-lg mr-2"></i>
|
||||
{$Locales.withdraw}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if $showDepositModal}
|
||||
<div
|
||||
class="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50 z-50"
|
||||
>
|
||||
<div
|
||||
class="bg-[#1c2333] p-8 rounded-lg shadow-2xl w-96 relative"
|
||||
in:scale={{ duration: 250, easing: quintOut }}
|
||||
out:scale={{ duration: 250, easing: quintOut }}
|
||||
>
|
||||
<h2 class="text-2xl font-bold text-green-400 mb-4 flex items-center">
|
||||
<i class="fa-duotone fa-arrow-up mr-2"></i>
|
||||
{$Locales.deposit_to_account}
|
||||
</h2>
|
||||
<div class="mb-4">
|
||||
<label class="block text-green-400 mb-2" for="DepositAmount"
|
||||
>{$Locales.deposit_amount}</label
|
||||
>
|
||||
<div class="relative">
|
||||
<input
|
||||
type="number"
|
||||
id="DepositAmount"
|
||||
bind:value={$transactionAmount}
|
||||
class="w-full bg-[#283040] text-white font-bold pl-4 pr-12 py-3 rounded-lg border border-green-400 focus:outline-none focus:border-green-600 transition-colors duration-500 placeholder-gray-500"
|
||||
placeholder="0"
|
||||
/>
|
||||
<i
|
||||
class="fa-duotone fa-dollar-sign absolute top-1/2 right-4 transform -translate-y-1/2 text-green-400"
|
||||
></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-center space-x-4">
|
||||
<button
|
||||
class="bg-gray-600 text-white py-2 px-4 rounded-lg flex items-center transition-colors duration-300"
|
||||
on:click={() => {
|
||||
showDepositModal.set(false);
|
||||
transactionAmount.set(0);
|
||||
}}
|
||||
>
|
||||
<i class="fa-duotone fa-times-circle text-lg mr-2"></i>
|
||||
{$Locales.cancel}
|
||||
</button>
|
||||
<button
|
||||
class="bg-green-600 text-white py-2 px-4 rounded-lg flex items-center transition-colors duration-300"
|
||||
on:click={depositToAccount}
|
||||
>
|
||||
<i class="fa-duotone fa-check-circle text-lg mr-2"></i>
|
||||
{$Locales.deposit}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
153
resources/[tools]/ps-banking/web/src/components/Bills.svelte
Normal file
153
resources/[tools]/ps-banking/web/src/components/Bills.svelte
Normal file
|
@ -0,0 +1,153 @@
|
|||
<script lang="ts">
|
||||
import { writable } from "svelte/store";
|
||||
import { onMount } from "svelte";
|
||||
import { fetchNui } from "../utils/fetchNui";
|
||||
import { slide, fade, scale } from "svelte/transition";
|
||||
import { quintOut } from "svelte/easing";
|
||||
import { Bills } from "../store/data";
|
||||
import { Notify, Locales, Currency } from "../store/data";
|
||||
|
||||
let transactions = Bills;
|
||||
let searchQuery = writable("");
|
||||
|
||||
$: filteredTransactions = $transactions.filter(
|
||||
(transaction) =>
|
||||
transaction.description
|
||||
.toLowerCase()
|
||||
.includes($searchQuery.toLowerCase()) ||
|
||||
transaction.type.toLowerCase().includes($searchQuery.toLowerCase())
|
||||
);
|
||||
|
||||
function formatDate(dateString: string) {
|
||||
const options: Intl.DateTimeFormatOptions = {
|
||||
year: "numeric",
|
||||
month: "numeric",
|
||||
day: "numeric",
|
||||
};
|
||||
return new Date(dateString).toLocaleDateString(undefined, options);
|
||||
}
|
||||
|
||||
async function payBill(transaction: { id: any; type: any }) {
|
||||
try {
|
||||
const result = await fetchNui("ps-banking:client:payBill", {
|
||||
id: transaction.id,
|
||||
});
|
||||
if (result) {
|
||||
Notify(
|
||||
`${$Locales.pay_invoice} #${transaction.id} ${$Locales.from} ${transaction.type}`,
|
||||
$Locales.payment_completed,
|
||||
"coins"
|
||||
);
|
||||
transactions.update((items) => {
|
||||
const index = items.findIndex((t) => transaction.id === t.id);
|
||||
if (index !== -1) {
|
||||
items.splice(index, 1);
|
||||
}
|
||||
return items;
|
||||
});
|
||||
return true;
|
||||
} else {
|
||||
Notify(`${$Locales.no_money_on_account}`, `${$Locales.error}`, "coins");
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
const response = await fetchNui("ps-banking:client:getBills", {});
|
||||
Bills.set(response);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="absolute w-full h-full bg-gray-800 text-white">
|
||||
<div
|
||||
class="absolute w-[90%] h-full p-6 overflow-auto left-[130px]"
|
||||
in:slide={{ duration: 1000, easing: quintOut }}
|
||||
>
|
||||
<div
|
||||
class="bg-gray-800/50 p-8 rounded-lg shadow-lg border border-blue-200/5"
|
||||
>
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<div class="flex items-center">
|
||||
<i class="fa-duotone fa-list text-3xl text-blue-200 mr-3"></i>
|
||||
<h2 class="text-3xl font-bold text-blue-200">{$Locales.bills}</h2>
|
||||
</div>
|
||||
<div class="bg-[#334155] rounded-full px-3 py-1 flex items-center">
|
||||
<i class="fa-duotone fa-file-invoice-dollar text-gray-400 mr-2"></i>
|
||||
<span class="text-sm text-gray-400 mr-2">{$Locales.total}</span>
|
||||
<span class="text-lg font-semibold text-white">
|
||||
{$transactions.length}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative mb-6">
|
||||
<i class="fa-duotone fa-search absolute left-4 top-4 text-gray-400"></i>
|
||||
<input
|
||||
type="text"
|
||||
class="w-full bg-gray-800 text-white pl-10 pr-4 py-3 rounded-lg border border-blue-200/10 focus:outline-none focus:border-blue-400/50 transition-colors duration-500 placeholder-gray-500"
|
||||
placeholder={$Locales.search_transactions}
|
||||
bind:value={$searchQuery}
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-6">
|
||||
{#each filteredTransactions as transaction (transaction.id)}
|
||||
<div
|
||||
class="p-4 bg-[#334155] rounded-lg flex justify-between items-center"
|
||||
out:slide={{ duration: 1000, easing: quintOut }}
|
||||
>
|
||||
<div class="flex flex-col space-y-1">
|
||||
<div class="flex items-center space-x-2">
|
||||
<i class="fa-duotone fa-file-invoice text-2xl text-[#f1f5f9]"
|
||||
></i>
|
||||
<span class="font-semibold text-[#f1f5f9]"
|
||||
>{transaction.description} #{transaction.id}</span
|
||||
>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<i class="fa-duotone fa-user text-sm text-gray-400"></i>
|
||||
<span class="text-sm text-gray-400">{transaction.type}</span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<i class="fa-duotone fa-clock text-xs text-gray-500"></i>
|
||||
<span class="text-xs text-gray-500"
|
||||
>{formatDate(transaction.date)}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right flex flex-col items-end space-y-1">
|
||||
<span
|
||||
class={`text-lg font-bold ${transaction.isIncome ? "text-green-500" : "text-red-500"}`}
|
||||
>
|
||||
<i class="fa-duotone fa-coins text-lg text-gray-400 mr-2"></i>
|
||||
{transaction.isIncome ? "+" : "-"}
|
||||
{transaction.amount.toLocaleString($Currency.lang, {
|
||||
style: "currency",
|
||||
currency: $Currency.currency,
|
||||
minimumFractionDigits: 0,
|
||||
})}
|
||||
</span>
|
||||
{#if !transaction.isPaid}
|
||||
<button
|
||||
class="bg-blue-600/10 border border-blue-500 hover:bg-blue-800/50 text-white font-bold py-2 px-4 rounded-lg flex items-center transition-colors duration-300"
|
||||
on:click={() => {
|
||||
payBill(transaction);
|
||||
}}
|
||||
>
|
||||
<i class="fa-duotone fa-money-check-edit text-lg mr-2"></i>
|
||||
{$Locales.pay_invoice}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
113
resources/[tools]/ps-banking/web/src/components/Heav.svelte
Normal file
113
resources/[tools]/ps-banking/web/src/components/Heav.svelte
Normal file
|
@ -0,0 +1,113 @@
|
|||
<script lang="ts">
|
||||
import { writable } from "svelte/store";
|
||||
import { slide } from "svelte/transition";
|
||||
import { quintOut } from "svelte/easing";
|
||||
import { fetchNui } from "../utils/fetchNui";
|
||||
import {
|
||||
Notify,
|
||||
currentCash,
|
||||
bankBalance,
|
||||
Locales,
|
||||
Currency,
|
||||
} from "../store/data";
|
||||
|
||||
let withdrawAmount = writable($bankBalance);
|
||||
$: newBank = $bankBalance - $withdrawAmount;
|
||||
|
||||
async function handleWithdraw() {
|
||||
if ($bankBalance < $withdrawAmount) {
|
||||
Notify(
|
||||
`${$Locales.withdraw_error} ${$withdrawAmount.toLocaleString(
|
||||
$Currency.lang,
|
||||
{
|
||||
style: "currency",
|
||||
currency: $Currency.currency,
|
||||
minimumFractionDigits: 0,
|
||||
}
|
||||
)}`,
|
||||
$Locales.error,
|
||||
"coins"
|
||||
);
|
||||
} else {
|
||||
Notify(
|
||||
`${$Locales.withdraw_success} ${$withdrawAmount.toLocaleString(
|
||||
$Currency.lang,
|
||||
{
|
||||
style: "currency",
|
||||
currency: $Currency.currency,
|
||||
minimumFractionDigits: 0,
|
||||
}
|
||||
)}`,
|
||||
$Locales.withdraw_success,
|
||||
"coins"
|
||||
);
|
||||
await fetchNui("ps-banking:client:ATMwithdraw", {
|
||||
amount: $withdrawAmount,
|
||||
});
|
||||
currentCash.update((cash) => cash + $withdrawAmount);
|
||||
bankBalance.update((balance) => balance - $withdrawAmount);
|
||||
withdrawAmount.set(0);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-label-has-associated-control -->
|
||||
<div class="absolute w-full h-full bg-gray-800 text-white">
|
||||
<div
|
||||
class="absolute w-[90%] h-full p-6 overflow-auto left-[130px]"
|
||||
in:slide={{ duration: 1000, easing: quintOut }}
|
||||
>
|
||||
<div
|
||||
class="bg-gray-800/50 p-8 rounded-lg shadow-lg border border-blue-200/5"
|
||||
>
|
||||
<h2 class="text-3xl font-bold mb-6">{$Locales.withdraw}</h2>
|
||||
|
||||
<div class="mb-12">
|
||||
<label class="block text-blue-200 mb-1">{$Locales.bank_balance}</label>
|
||||
<div class="flex items-center text-2xl font-semibold">
|
||||
<i class="fa-duotone fa-university text-gray-400 mr-2"></i>
|
||||
{$bankBalance.toLocaleString($Currency.lang, {
|
||||
style: "currency",
|
||||
currency: $Currency.currency,
|
||||
minimumFractionDigits: 0,
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-12">
|
||||
<label class="block text-blue-200 mb-1">{$Locales.amount}</label>
|
||||
<div class="relative">
|
||||
<i
|
||||
class="fa-duotone fa-money-bill-wave absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400"
|
||||
></i>
|
||||
<input
|
||||
type="number"
|
||||
class="w-full rounded bg-gray-700/50 text-white pl-10 pr-4 py-3 border border-blue-200/10 rounded-lg focus:outline-none
|
||||
focus:border-blue-400/50 transition-colors duration-500"
|
||||
bind:value={$withdrawAmount}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-12">
|
||||
<label class="block text-blue-200 mb-1">{$Locales.new_bank}</label>
|
||||
<div class="flex items-center text-2xl font-semibold">
|
||||
<i class="fa-duotone fa-coins text-gray-400 mr-2"></i>
|
||||
{newBank.toLocaleString($Currency.lang, {
|
||||
style: "currency",
|
||||
currency: $Currency.currency,
|
||||
minimumFractionDigits: 0,
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="w-full bg-blue-600/10 hover:bg-blue-700/10 text-white font-bold py-3 rounded transition duration-300 flex items-center justify-center border border-blue-500/50"
|
||||
on:click={handleWithdraw}
|
||||
>
|
||||
<i class="fa-duotone fa-money-check-edit text-lg mr-2"></i>
|
||||
{$Locales.withdraw_button}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
206
resources/[tools]/ps-banking/web/src/components/History.svelte
Normal file
206
resources/[tools]/ps-banking/web/src/components/History.svelte
Normal file
|
@ -0,0 +1,206 @@
|
|||
<script lang="ts">
|
||||
import { writable } from "svelte/store";
|
||||
import { onMount } from "svelte";
|
||||
import { quintOut } from "svelte/easing";
|
||||
import { slide, fade, scale } from "svelte/transition";
|
||||
import { fetchNui } from "../utils/fetchNui";
|
||||
import {
|
||||
showOverview,
|
||||
showBills,
|
||||
showHistory,
|
||||
notifications,
|
||||
Bills,
|
||||
Notify,
|
||||
Transactions,
|
||||
Locales,
|
||||
Currency,
|
||||
type Notification,
|
||||
} from "../store/data";
|
||||
|
||||
// Sample data
|
||||
let transactions = Transactions;
|
||||
let searchQuery = writable("");
|
||||
let showDeleteAllModal = writable(false);
|
||||
|
||||
$: filteredTransactions = $transactions.filter(
|
||||
(transaction) =>
|
||||
transaction.description
|
||||
.toLowerCase()
|
||||
.includes($searchQuery.toLowerCase()) ||
|
||||
transaction.type.toLowerCase().includes($searchQuery.toLowerCase())
|
||||
);
|
||||
|
||||
function confirmDeleteAllTransactions() {
|
||||
showDeleteAllModal.set(true);
|
||||
}
|
||||
|
||||
async function deleteAllTransactions() {
|
||||
if ($transactions.length === 0) {
|
||||
Notify($Locales.history_empty, $Locales.error, "file-invoice");
|
||||
showDeleteAllModal.set(false);
|
||||
} else {
|
||||
transactions.set([]);
|
||||
showDeleteAllModal.set(false);
|
||||
Notify($Locales.all_history_deleted, $Locales.success, "file-invoice");
|
||||
try {
|
||||
const history = await fetchNui("ps-banking:client:deleteHistory", {});
|
||||
transactions.set([]);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function formatDate(dateString: string) {
|
||||
const options: Intl.DateTimeFormatOptions = {
|
||||
year: "numeric",
|
||||
month: "numeric",
|
||||
day: "numeric",
|
||||
};
|
||||
return new Date(dateString).toLocaleDateString(undefined, options);
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
const history = await fetchNui("ps-banking:client:getHistory", {});
|
||||
transactions.set(history);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="absolute w-full h-full bg-gray-800">
|
||||
<div
|
||||
class="absolute w-[90%] h-full p-6 overflow-auto left-[130px] text-blue-200"
|
||||
in:slide={{ duration: 1000, easing: quintOut }}
|
||||
>
|
||||
<div
|
||||
class="bg-gray-800/50 p-8 rounded-lg shadow-lg border border-blue-200/5"
|
||||
>
|
||||
<div class="flex items-center mb-4">
|
||||
<i class="fa-duotone fa-list text-2xl text-blue-400 mr-3"></i>
|
||||
<h2 class="text-2xl font-bold">{$Locales.history}</h2>
|
||||
</div>
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<div class="flex items-center">
|
||||
<i class="fa-duotone fa-wallet text-xl text-gray-400 mr-2"></i>
|
||||
<span class="text-gray-400">{$Locales.total}</span>
|
||||
</div>
|
||||
<div class="absolute right-16 top-10">
|
||||
<i class="fa-duotone fa-receipt text-xl text-gray-400 mr-2"></i>
|
||||
<span class="text-xl text-white font-semibold">
|
||||
{filteredTransactions.length}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
class="bg-gray-700/50 text-blue-200 py-2 px-4 rounded-lg flex items-center hover:bg-gray-500/50 transition duration-500 border border-gray-500/20"
|
||||
on:click={confirmDeleteAllTransactions}
|
||||
>
|
||||
<i class="fa-duotone fa-trash-alt mr-2"
|
||||
></i>{$Locales.delete_all_transactions}
|
||||
</button>
|
||||
</div>
|
||||
<div class="relative mb-6">
|
||||
<i class="fa-duotone fa-search absolute left-4 top-4 text-gray-400"></i>
|
||||
<input
|
||||
type="text"
|
||||
class="w-full rounded bg-gray-700/50 text-white pl-10 pr-4 py-3 border border-blue-200/10 rounded-lg focus:outline-none
|
||||
focus:border-blue-400/50 transition-colors duration-500"
|
||||
placeholder={$Locales.search_transactions}
|
||||
bind:value={$searchQuery}
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
{#each filteredTransactions as transaction (transaction.id)}
|
||||
<div
|
||||
class="bg-[#334155] p-4 rounded-lg shadow-md transition duration-300 border border-blue-200/5"
|
||||
out:slide={{ duration: 500 }}
|
||||
>
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="flex items-center">
|
||||
<i
|
||||
class={`fa-duotone ${transaction.isIncome ? "fa-arrow-down-to-arc" : "fa-arrow-up-from-arc"} text-xl mr-3 ${transaction.isIncome ? "text-green-400" : "text-red-400"}`}
|
||||
></i>
|
||||
<div>
|
||||
<div class="flex items-center">
|
||||
<i
|
||||
class="fa-duotone fa-file-invoice text-lg text-gray-300 mr-2"
|
||||
></i>
|
||||
<p class="text-lg font-bold">
|
||||
{transaction.description} #{transaction.id}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<i
|
||||
class="fa-duotone fa-exchange-alt text-sm text-gray-400 mr-2"
|
||||
></i>
|
||||
<p class="text-sm text-gray-400">{transaction.type}</p>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<i class="fa-duotone fa-clock text-xs text-gray-500 mr-2"
|
||||
></i>
|
||||
<p class="text-xs text-gray-500">
|
||||
{formatDate(transaction.date)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<div class="flex items-center">
|
||||
<i class="fa-duotone fa-coins text-lg text-gray-400 mr-2"></i>
|
||||
<p
|
||||
class={`text-lg font-bold ${transaction.isIncome ? "text-green-500" : "text-red-500"}`}
|
||||
>
|
||||
{transaction.isIncome ? "+" : "-"}
|
||||
{transaction.amount.toLocaleString($Currency.lang, {
|
||||
style: "currency",
|
||||
currency: $Currency.currency,
|
||||
minimumFractionDigits: 0,
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{#if $showDeleteAllModal}
|
||||
<div
|
||||
class="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50"
|
||||
>
|
||||
<div
|
||||
class="bg-gray-700 p-8 rounded-lg shadow-lg w-96"
|
||||
in:scale={{ duration: 250, easing: quintOut }}
|
||||
out:scale={{ duration: 250, easing: quintOut }}
|
||||
>
|
||||
<div class="flex items-center mb-4">
|
||||
<i class="fa-duotone fa-question-circle text-3xl text-blue-400 mr-3"
|
||||
></i>
|
||||
<h2 class="text-2xl text-blue-200 font-bold">
|
||||
{$Locales.are_you_sure}
|
||||
</h2>
|
||||
</div>
|
||||
<p class="text-gray-300 mb-6">
|
||||
{$Locales.delete_confirmation}
|
||||
</p>
|
||||
<div class="flex justify-between items-center">
|
||||
<button
|
||||
class="flex items-center bg-red-600 hover:bg-red-700 text-white py-2 px-4 rounded focus:outline-none"
|
||||
on:click={() => showDeleteAllModal.set(false)}
|
||||
>
|
||||
<i class="fa-duotone fa-times-circle mr-2"></i>{$Locales.cancel}
|
||||
</button>
|
||||
<button
|
||||
class="flex items-center bg-blue-600 hover:bg-blue-700 text-white py-2 px-4 rounded focus:outline-none"
|
||||
on:click={deleteAllTransactions}
|
||||
>
|
||||
<i class="fa-duotone fa-check-circle mr-2"></i>{$Locales.confirm}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
115
resources/[tools]/ps-banking/web/src/components/Indseat.svelte
Normal file
115
resources/[tools]/ps-banking/web/src/components/Indseat.svelte
Normal file
|
@ -0,0 +1,115 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import { writable } from "svelte/store";
|
||||
import { slide } from "svelte/transition";
|
||||
import { quintOut } from "svelte/easing";
|
||||
import {
|
||||
Notify,
|
||||
currentCash,
|
||||
bankBalance,
|
||||
Locales,
|
||||
Currency,
|
||||
} from "../store/data";
|
||||
import { fetchNui } from "../utils/fetchNui";
|
||||
|
||||
let depositAmount = writable($currentCash);
|
||||
|
||||
$: newCash = $currentCash - $depositAmount;
|
||||
|
||||
async function handleDeposit() {
|
||||
if ($currentCash < $depositAmount) {
|
||||
Notify(
|
||||
`${$Locales.deposit_error} ${$depositAmount.toLocaleString(
|
||||
$Currency.lang,
|
||||
{
|
||||
style: "currency",
|
||||
currency: $Currency.currency,
|
||||
minimumFractionDigits: 0,
|
||||
}
|
||||
)}`,
|
||||
$Locales.error,
|
||||
"coins"
|
||||
);
|
||||
} else {
|
||||
Notify(
|
||||
`${$Locales.deposit_success} ${$depositAmount.toLocaleString(
|
||||
$Currency.lang,
|
||||
{
|
||||
style: "currency",
|
||||
currency: $Currency.currency,
|
||||
minimumFractionDigits: 0,
|
||||
}
|
||||
)} `,
|
||||
$Locales.deposit_success,
|
||||
"coins"
|
||||
);
|
||||
await fetchNui("ps-banking:client:ATMdeposit", {
|
||||
amount: $depositAmount,
|
||||
});
|
||||
currentCash.update((cash) => cash - $depositAmount);
|
||||
bankBalance.update((balance) => balance + $depositAmount);
|
||||
depositAmount.set(0);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-label-has-associated-control -->
|
||||
<div class="absolute w-full h-full bg-gray-800 text-white">
|
||||
<div
|
||||
class="absolute w-[90%] h-full p-6 overflow-auto left-[130px]"
|
||||
in:slide={{ duration: 1000, easing: quintOut }}
|
||||
>
|
||||
<div
|
||||
class="bg-gray-800/50 p-8 rounded-lg shadow-lg border border-blue-200/5"
|
||||
>
|
||||
<h2 class="text-3xl font-bold mb-6">{$Locales.deposit}</h2>
|
||||
|
||||
<div class="mb-12">
|
||||
<label class="block text-blue-200 mb-1">{$Locales.current_cash}</label>
|
||||
<div class="flex items-center text-2xl font-semibold">
|
||||
<i class="fa-duotone fa-wallet text-gray-400 mr-2"></i>
|
||||
{$currentCash.toLocaleString($Currency.lang, {
|
||||
style: "currency",
|
||||
currency: $Currency.currency,
|
||||
minimumFractionDigits: 0,
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-12">
|
||||
<label class="block text-blue-200 mb-1">{$Locales.amount}</label>
|
||||
<div class="relative">
|
||||
<i
|
||||
class="fa-duotone fa-money-bill-wave absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400"
|
||||
></i>
|
||||
<input
|
||||
type="number"
|
||||
class="w-full rounded bg-gray-700/50 text-white pl-10 pr-4 py-3 border border-blue-200/10 rounded-lg focus:outline-none
|
||||
focus:border-blue-400/50 transition-colors duration-500"
|
||||
bind:value={$depositAmount}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-12">
|
||||
<label class="block text-blue-200 mb-1">{$Locales.new_cash}</label>
|
||||
<div class="flex items-center text-2xl font-semibold">
|
||||
<i class="fa-duotone fa-coins text-gray-400 mr-2"></i>
|
||||
{newCash.toLocaleString($Currency.lang, {
|
||||
style: "currency",
|
||||
currency: $Currency.currency,
|
||||
minimumFractionDigits: 0,
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="w-full bg-blue-600/10 hover:bg-blue-700/10 text-white font-bold py-3 rounded transition duration-300 flex items-center justify-center border border-blue-500/50"
|
||||
on:click={handleDeposit}
|
||||
>
|
||||
<i class="fa-duotone fa-money-check-edit text-lg mr-2"></i>
|
||||
{$Locales.deposit_button}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
297
resources/[tools]/ps-banking/web/src/components/Main.svelte
Normal file
297
resources/[tools]/ps-banking/web/src/components/Main.svelte
Normal file
|
@ -0,0 +1,297 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import { useNuiEvent } from "../utils/useNuiEvent";
|
||||
import { fetchNui } from "../utils/fetchNui";
|
||||
import { visibility } from "../store/stores";
|
||||
import OverviewPage from "./Overview.svelte";
|
||||
import BillsPage from "./Bills.svelte";
|
||||
import HistoryPage from "./History.svelte";
|
||||
import HeavPage from "./Heav.svelte";
|
||||
import IndseatPage from "./Indseat.svelte";
|
||||
import StatsPage from "./Stats.svelte";
|
||||
import AccountsPage from "./Accounts.svelte";
|
||||
import { slide, fade, scale } from "svelte/transition";
|
||||
import { quintOut } from "svelte/easing";
|
||||
import {
|
||||
showOverview,
|
||||
showBills,
|
||||
showHistory,
|
||||
showHeav,
|
||||
showIndseat,
|
||||
showStats,
|
||||
showAccounts,
|
||||
Locales,
|
||||
bankBalance,
|
||||
currentCash,
|
||||
} from "../store/data";
|
||||
import { writable } from "svelte/store";
|
||||
|
||||
async function updateBalances() {
|
||||
try {
|
||||
const response = await fetchNui("ps-banking:client:getMoneyTypes", {});
|
||||
const bank = response.find(
|
||||
(item: { name: string }) => item.name === "bank"
|
||||
);
|
||||
const cash = response.find(
|
||||
(item: { name: string }) => item.name === "cash"
|
||||
);
|
||||
if (bank) {
|
||||
bankBalance.set(bank.amount);
|
||||
}
|
||||
if (cash) {
|
||||
currentCash.set(cash.amount);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
updateBalances();
|
||||
try {
|
||||
const response = await fetchNui("ps-banking:client:getLocales", {});
|
||||
Locales.set(response);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="h-screen w-screen flex flex-col items-center justify-center select-none overflow-hidden"
|
||||
>
|
||||
<div
|
||||
class="absolute w-[80%] h-[90%] rounded-xl overflow-hidden"
|
||||
in:scale={{ duration: 1000, easing: quintOut }}
|
||||
out:fade={{ duration: 1000, easing: quintOut }}
|
||||
>
|
||||
{#if $showOverview}
|
||||
<OverviewPage />
|
||||
{:else if $showBills}
|
||||
<BillsPage />
|
||||
{:else if $showHistory}
|
||||
<HistoryPage />
|
||||
{:else if $showHeav}
|
||||
<HeavPage />
|
||||
{:else if $showIndseat}
|
||||
<IndseatPage />
|
||||
{:else if $showStats}
|
||||
<StatsPage />
|
||||
{:else if $showAccounts}
|
||||
<AccountsPage />
|
||||
{/if}
|
||||
<!-- SideBar -->
|
||||
<div
|
||||
class="relative bg-gray-700/90 left-0 border border-gray-600/40 h-full w-28 flex flex-col items-center rounded-l-xl overflow-hidden"
|
||||
>
|
||||
<div class="relative h-full w-full top-3 left-[2px] space-y-2">
|
||||
<label
|
||||
class="text-white font-bold p-0 rounded flex flex-col items-center uppercase w-[97%]"
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="radio"
|
||||
value="overview"
|
||||
class="hidden peer"
|
||||
checked={$showOverview}
|
||||
on:change={() => {
|
||||
showOverview.set(true);
|
||||
showBills.set(false);
|
||||
showHistory.set(false);
|
||||
showHeav.set(false);
|
||||
showIndseat.set(false);
|
||||
showStats.set(false);
|
||||
showAccounts.set(false);
|
||||
}}
|
||||
/>
|
||||
<span
|
||||
class="w-[97%] relative flex flex-col items-center text-gray-300 py-4 peer-checked:shadow-md transition-all duration-500 rounded-xl
|
||||
peer-checked:text-blue-400 peer-checked:shadow-lg hover:text-blue-300 duration-500 peer-checked:bg-gray-600 hover:cursor-pointer hover:bg-gray-800/80"
|
||||
>
|
||||
<i class="fa-duotone fa-house text-3xl text-blue-300 mb-2"></i>
|
||||
<span class="relative">{$Locales.overview}</span>
|
||||
</span>
|
||||
</label>
|
||||
<label
|
||||
class="text-white font-bold p-0 rounded flex flex-col items-center uppercase w-[97%]"
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="radio"
|
||||
value="send"
|
||||
class="hidden peer"
|
||||
checked={$showBills}
|
||||
on:change={() => {
|
||||
showOverview.set(false);
|
||||
showBills.set(true);
|
||||
showHistory.set(false);
|
||||
showHeav.set(false);
|
||||
showIndseat.set(false);
|
||||
showStats.set(false);
|
||||
showAccounts.set(false);
|
||||
}}
|
||||
/>
|
||||
<span
|
||||
class="w-[97%] relative flex flex-col items-center text-gray-300 py-4 peer-checked:shadow-md transition-all duration-500 rounded-xl
|
||||
peer-checked:text-blue-400 peer-checked:shadow-lg hover:text-blue-300 duration-500 peer-checked:bg-gray-600 hover:cursor-pointer hover:bg-gray-800/80"
|
||||
>
|
||||
<i class="fa-duotone fa-file-invoice text-3xl text-blue-300 mb-2"
|
||||
></i>
|
||||
<span class="relative">{$Locales.bills}</span>
|
||||
</span>
|
||||
</label>
|
||||
<label
|
||||
class="text-white font-bold p-0 rounded flex flex-col items-center uppercase w-[97%]"
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="radio"
|
||||
value="history"
|
||||
class="hidden peer"
|
||||
checked={$showHistory}
|
||||
on:change={() => {
|
||||
showOverview.set(false);
|
||||
showBills.set(false);
|
||||
showHistory.set(true);
|
||||
showHeav.set(false);
|
||||
showIndseat.set(false);
|
||||
showStats.set(false);
|
||||
showAccounts.set(false);
|
||||
}}
|
||||
/>
|
||||
<span
|
||||
class="w-[97%] relative flex flex-col items-center text-gray-300 py-4 peer-checked:shadow-md transition-all duration-500 rounded-xl
|
||||
peer-checked:text-blue-400 peer-checked:shadow-lg hover:text-blue-300 duration-500 peer-checked:bg-gray-600 hover:cursor-pointer hover:bg-gray-800/80"
|
||||
>
|
||||
<i class="fa-duotone fa-circle-dollar text-3xl text-blue-300 mb-2"
|
||||
></i>
|
||||
<span class="relative">{$Locales.history}</span>
|
||||
</span>
|
||||
</label>
|
||||
<label
|
||||
class="text-white font-bold p-0 rounded flex flex-col items-center uppercase w-[97%]"
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="radio"
|
||||
value="control"
|
||||
class="hidden peer"
|
||||
checked={$showHeav}
|
||||
on:change={() => {
|
||||
showOverview.set(false);
|
||||
showBills.set(false);
|
||||
showHistory.set(false);
|
||||
showHeav.set(true);
|
||||
showIndseat.set(false);
|
||||
showStats.set(false);
|
||||
showAccounts.set(false);
|
||||
}}
|
||||
/>
|
||||
<span
|
||||
class="w-[97%] relative flex flex-col items-center text-gray-300 py-4 peer-checked:shadow-md transition-all duration-500 rounded-xl
|
||||
peer-checked:text-blue-400 peer-checked:shadow-lg hover:text-blue-300 duration-500 peer-checked:bg-gray-600 hover:cursor-pointer hover:bg-gray-800/80"
|
||||
>
|
||||
<i class="fa-duotone fa-minus text-3xl text-blue-300 mb-2"></i>
|
||||
<span class="relative">{$Locales.withdraw}</span>
|
||||
</span>
|
||||
</label>
|
||||
<label
|
||||
class="text-white font-bold p-0 rounded flex flex-col items-center uppercase w-[97%]"
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="radio"
|
||||
value="control"
|
||||
class="hidden peer"
|
||||
checked={$showIndseat}
|
||||
on:change={() => {
|
||||
showOverview.set(false);
|
||||
showBills.set(false);
|
||||
showHistory.set(false);
|
||||
showHeav.set(false);
|
||||
showIndseat.set(true);
|
||||
showStats.set(false);
|
||||
showAccounts.set(false);
|
||||
}}
|
||||
/>
|
||||
<span
|
||||
class="w-[97%] relative flex flex-col items-center text-gray-300 py-4 peer-checked:shadow-md transition-all duration-500 rounded-xl
|
||||
peer-checked:text-blue-400 peer-checked:shadow-lg hover:text-blue-300 duration-500 peer-checked:bg-gray-600 hover:cursor-pointer hover:bg-gray-800/80"
|
||||
>
|
||||
<i class="fa-duotone fa-plus text-3xl text-blue-300 mb-2"></i>
|
||||
<span class="relative">{$Locales.deposit}</span>
|
||||
</span>
|
||||
</label>
|
||||
<label
|
||||
class="text-white font-bold p-0 rounded flex flex-col items-center uppercase w-[97%]"
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="radio"
|
||||
value="control"
|
||||
class="hidden peer"
|
||||
checked={$showStats}
|
||||
on:change={() => {
|
||||
showOverview.set(false);
|
||||
showBills.set(false);
|
||||
showHistory.set(false);
|
||||
showHeav.set(false);
|
||||
showIndseat.set(false);
|
||||
showStats.set(true);
|
||||
showAccounts.set(false);
|
||||
}}
|
||||
/>
|
||||
<span
|
||||
class="w-[97%] relative flex flex-col items-center text-gray-300 py-4 peer-checked:shadow-md transition-all duration-500 rounded-xl
|
||||
peer-checked:text-blue-400 peer-checked:shadow-lg hover:text-blue-300 duration-500 peer-checked:bg-gray-600 hover:cursor-pointer hover:bg-gray-800/80"
|
||||
>
|
||||
<i class="fa-duotone fa-chart-simple text-3xl text-blue-300 mb-2"
|
||||
></i>
|
||||
<span class="relative">{$Locales.stats}</span>
|
||||
</span>
|
||||
</label>
|
||||
<label
|
||||
class="text-white font-bold p-0 rounded flex flex-col items-center uppercase w-[97%]"
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="radio"
|
||||
value="control"
|
||||
class="hidden peer"
|
||||
checked={$showAccounts}
|
||||
on:change={() => {
|
||||
showOverview.set(false);
|
||||
showBills.set(false);
|
||||
showHistory.set(false);
|
||||
showHeav.set(false);
|
||||
showIndseat.set(false);
|
||||
showStats.set(false);
|
||||
showAccounts.set(true);
|
||||
}}
|
||||
/>
|
||||
<span
|
||||
class="w-[97%] relative flex flex-col items-center text-gray-300 py-4 peer-checked:shadow-md transition-all duration-500 rounded-xl
|
||||
peer-checked:text-blue-400 peer-checked:shadow-lg hover:text-blue-300 duration-500 peer-checked:bg-gray-600 hover:cursor-pointer hover:bg-gray-800/80"
|
||||
>
|
||||
<i class="fa-duotone fa-users text-3xl text-blue-300 mb-2"></i>
|
||||
<span class="relative">{$Locales.accounts}</span>
|
||||
</span>
|
||||
</label>
|
||||
<!-- Close -->
|
||||
<div class="relative -bottom-48 left-[.5px]">
|
||||
<button
|
||||
class="w-[95%] text-blue-200 font-bold uppercase p-5 rounded-lg hover:bg-gray-800/80 duration-500 h-[100px] flex flex-col items-center"
|
||||
on:click={() => {
|
||||
fetchNui("ps-banking:client:hideUI");
|
||||
visibility.set(false);
|
||||
}}
|
||||
>
|
||||
<i class="fa-duotone fa-circle-xmark text-3xl text-blue-300 mb-2"
|
||||
></i>
|
||||
<span class="relative">{$Locales.close}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
755
resources/[tools]/ps-banking/web/src/components/Overview.svelte
Normal file
755
resources/[tools]/ps-banking/web/src/components/Overview.svelte
Normal file
|
@ -0,0 +1,755 @@
|
|||
<script lang="ts">
|
||||
import { writable } from "svelte/store";
|
||||
import { onMount } from "svelte";
|
||||
import Chart from "chart.js/auto";
|
||||
import { fetchNui } from "../utils/fetchNui";
|
||||
import { quintOut } from "svelte/easing";
|
||||
import { slide, fade, scale } from "svelte/transition";
|
||||
import {
|
||||
showOverview,
|
||||
showBills,
|
||||
showHistory,
|
||||
showHeav,
|
||||
notifications,
|
||||
Bills,
|
||||
Notify,
|
||||
Transactions,
|
||||
currentCash,
|
||||
bankBalance,
|
||||
Locales,
|
||||
Currency,
|
||||
type Notification,
|
||||
} from "../store/data";
|
||||
|
||||
let notificationId = 0;
|
||||
let transactions = Bills;
|
||||
let phone = false;
|
||||
let showSureModalBills = writable(false);
|
||||
let showTransferModal = writable(false);
|
||||
let transferData = writable({
|
||||
idOrPhone: "",
|
||||
amount: 0,
|
||||
confirm: false,
|
||||
contactType: "none",
|
||||
});
|
||||
|
||||
let weeklyData = writable({
|
||||
totalReceived: 0,
|
||||
totalUsed: 0,
|
||||
});
|
||||
|
||||
let chart: Chart;
|
||||
let chartCanvas: HTMLCanvasElement;
|
||||
|
||||
async function fetchWeeklySummary() {
|
||||
try {
|
||||
const response = await fetchNui("ps-banking:client:getWeeklySummary", {});
|
||||
if (response) {
|
||||
weeklyData.set(response);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
async function updateBalances() {
|
||||
try {
|
||||
const response = await fetchNui("ps-banking:client:getMoneyTypes", {});
|
||||
const bank = response.find(
|
||||
(item: { name: string }) => item.name === "bank"
|
||||
);
|
||||
const cash = response.find(
|
||||
(item: { name: string }) => item.name === "cash"
|
||||
);
|
||||
if (bank) {
|
||||
bankBalance.set(bank.amount);
|
||||
}
|
||||
if (cash) {
|
||||
currentCash.set(cash.amount);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
async function payAllBills() {
|
||||
const success = await fetchNui("ps-banking:client:payAllBills", {});
|
||||
if (success) {
|
||||
await getBills();
|
||||
Notify(
|
||||
$Locales.pay_all_bills_success,
|
||||
$Locales.payment_completed,
|
||||
"money-bill"
|
||||
);
|
||||
} else {
|
||||
Notify(
|
||||
$Locales.pay_all_bills_error,
|
||||
$Locales.error,
|
||||
"circle-exclamation"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function openModal() {
|
||||
showTransferModal.set(true);
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
showTransferModal.set(false);
|
||||
transferData.set({
|
||||
idOrPhone: "",
|
||||
amount: 0,
|
||||
confirm: false,
|
||||
contactType: "none",
|
||||
});
|
||||
}
|
||||
|
||||
async function getBills() {
|
||||
try {
|
||||
const response = await fetchNui("ps-banking:client:getBills", {});
|
||||
Bills.set(response);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
async function getHistory() {
|
||||
try {
|
||||
const history = await fetchNui("ps-banking:client:getHistory", {});
|
||||
Transactions.set(history);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
async function confirmTransfer(id: any, amount: any, method: any) {
|
||||
try {
|
||||
const response = await fetchNui("ps-banking:client:transferMoney", {
|
||||
id: id,
|
||||
amount: amount,
|
||||
method: method,
|
||||
});
|
||||
if (response.success) {
|
||||
Notify(response.message, $Locales.payment_completed, "user");
|
||||
} else {
|
||||
Notify(response.message, $Locales.error, "user");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
transferData.update((data) => {
|
||||
data.confirm = true;
|
||||
return data;
|
||||
});
|
||||
showTransferModal.set(false);
|
||||
transferData.set({
|
||||
idOrPhone: "",
|
||||
amount: 0,
|
||||
confirm: false,
|
||||
contactType: "none",
|
||||
});
|
||||
}
|
||||
|
||||
let bankData = {
|
||||
balance: $bankBalance,
|
||||
cash: $currentCash,
|
||||
transactions: $Transactions,
|
||||
};
|
||||
|
||||
$: bankData = {
|
||||
balance: $bankBalance,
|
||||
cash: $currentCash,
|
||||
transactions: $Transactions,
|
||||
};
|
||||
|
||||
async function heav() {
|
||||
try {
|
||||
const response = await fetchNui("ps-banking:client:ATMwithdraw", {
|
||||
amount: $bankBalance,
|
||||
});
|
||||
if (response) {
|
||||
updateStuff();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
async function deposit() {
|
||||
try {
|
||||
const response = await fetchNui("ps-banking:client:ATMdeposit", {
|
||||
amount: $currentCash,
|
||||
});
|
||||
if (response) {
|
||||
updateStuff();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
function createChart() {
|
||||
if (chartCanvas) {
|
||||
chart = new Chart(chartCanvas, {
|
||||
type: "bar",
|
||||
data: {
|
||||
labels: [$Locales.income, $Locales.expenses],
|
||||
datasets: [
|
||||
{
|
||||
label: $Locales.weekly_summary,
|
||||
data: [0, 0],
|
||||
backgroundColor: ["#3b82f6", "#ef4444"],
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
$: {
|
||||
weeklyData.subscribe((data) => {
|
||||
if (chart) {
|
||||
chart.data.datasets[0].data = [data.totalReceived, data.totalUsed];
|
||||
chart.update();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function updateStuff() {
|
||||
// Hot update
|
||||
await getBills();
|
||||
await getHistory();
|
||||
await fetchWeeklySummary();
|
||||
await updateBalances();
|
||||
}
|
||||
|
||||
async function phoneOption() {
|
||||
try {
|
||||
const response = await fetchNui("ps-banking:client:phoneOption", {});
|
||||
phone = response
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
createChart();
|
||||
updateStuff();
|
||||
updateStuff();
|
||||
phoneOption();
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="absolute w-full h-full bg-gray-800">
|
||||
<div
|
||||
class="absolute top-0 left-[240px] w-screen h-screen overflow-hidden p-6 text-white"
|
||||
in:slide={{ duration: 1000, easing: quintOut }}
|
||||
>
|
||||
<!-- Quick Actions -->
|
||||
<div class="mb-8">
|
||||
<div class="text-xl mb-4 text-blue-200">{$Locales.total_balance}</div>
|
||||
<div class="text-3xl font-bold mb-8 text-blue-300">
|
||||
{$bankBalance.toLocaleString($Currency.lang, {
|
||||
style: "currency",
|
||||
currency: $Currency.currency,
|
||||
minimumFractionDigits: 0,
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<div class="text-2xl mb-4 text-blue-200">{$Locales.quick_actions}</div>
|
||||
<div class="flex space-x-4">
|
||||
<div
|
||||
class="flex flex-col items-center bg-gray-700 rounded-xl p-4 w-[280px]"
|
||||
>
|
||||
<i
|
||||
class="fa-duotone fa-arrow-right-arrow-left text-4xl mb-2 text-blue-400"
|
||||
></i>
|
||||
<div class="text-2xl font-bold mb-2 text-blue-200">
|
||||
{$Locales.transfer_money}
|
||||
</div>
|
||||
<div class="text-sm mb-2 text-gray-400">
|
||||
{$Locales.easy_transfer}
|
||||
</div>
|
||||
<button
|
||||
class="bg-blue-600/10 border border-blue-500 hover:bg-blue-800/50 text-white font-bold py-2 px-4 mt-4 duration-500 rounded-lg cursor-pointer"
|
||||
on:click={openModal}
|
||||
>
|
||||
{$Locales.transfer}
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="flex flex-col items-center bg-gray-700 rounded-xl p-4 w-[280px]"
|
||||
>
|
||||
<i
|
||||
class="fa-duotone fa-file-invoice-dollar text-4xl mb-2 text-blue-400"
|
||||
></i>
|
||||
<div class="text-2xl font-bold mb-2 text-blue-200">
|
||||
{$Locales.pay_bills}
|
||||
</div>
|
||||
<div class="text-sm mb-2 text-gray-400">
|
||||
{$Locales.pay_pending_bills}
|
||||
</div>
|
||||
<button
|
||||
class="relative -bottom-auto bg-blue-600/10 border border-blue-500 hover:bg-blue-800/50 text-white font-bold py-2 px-8 mt-4 duration-500 rounded-lg cursor-pointer"
|
||||
on:click={() => {
|
||||
showSureModalBills.set(true);
|
||||
}}
|
||||
>
|
||||
{$Locales.pay}
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="flex flex-col items-center bg-gray-700 rounded-xl p-4 w-[280px]"
|
||||
>
|
||||
<i class="fa-duotone fa-credit-card text-4xl mb-2 text-blue-400"></i>
|
||||
<div class="text-2xl font-bold mb-2 text-blue-200">
|
||||
{$Locales.withdraw_all_money}
|
||||
</div>
|
||||
<div class="text-sm mb-2 text-gray-400">
|
||||
{$Locales.withdraw_all_from_account}
|
||||
</div>
|
||||
<button
|
||||
class="bg-blue-600/10 border border-blue-500 hover:bg-blue-800/50 text-white font-bold py-2 px-4 mt-4 duration-500 rounded-lg cursor-pointer"
|
||||
on:click={() => {
|
||||
if ($bankBalance <= 0) {
|
||||
Notify(
|
||||
$Locales.no_money_on_account,
|
||||
$Locales.error,
|
||||
"credit-card"
|
||||
);
|
||||
} else {
|
||||
Notify(
|
||||
$Locales.withdraw_all_success,
|
||||
$Locales.success,
|
||||
"credit-card"
|
||||
);
|
||||
setTimeout(() => {
|
||||
heav();
|
||||
}, 200);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{$Locales.withdraw}
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="flex flex-col items-center bg-gray-700 rounded-xl p-4 w-[280px]"
|
||||
>
|
||||
<i class="fa-duotone fa-piggy-bank text-4xl mb-2 text-blue-400"></i>
|
||||
<div class="text-2xl font-bold mb-2 text-blue-200">
|
||||
{$Locales.deposit_cash}
|
||||
</div>
|
||||
<div class="text-sm mb-2 text-gray-400">
|
||||
{$Locales.deposit_all_cash}
|
||||
</div>
|
||||
<button
|
||||
class="bg-blue-600/10 border border-blue-500 hover:bg-blue-800/50 text-white font-bold py-2 px-4 mt-4 duration-500 rounded-lg cursor-pointer"
|
||||
on:click={() => {
|
||||
if ($currentCash <= 0) {
|
||||
Notify($Locales.no_cash_on_you, $Locales.error, "coins");
|
||||
} else {
|
||||
Notify($Locales.deposit_all_success, $Locales.success, "coins");
|
||||
setTimeout(() => {
|
||||
deposit();
|
||||
}, 200);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{$Locales.deposit}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Lower Section -->
|
||||
<div class="flex space-x-4 mt-4">
|
||||
<!-- Weekly Summary -->
|
||||
<div class="bg-gray-700 rounded-xl p-6 w-[380px] h-[400px] flex-none">
|
||||
<div class="flex items-center mb-4">
|
||||
<i class="fa-duotone fa-calendar-week text-2xl text-blue-400 mr-2"
|
||||
></i>
|
||||
<span class="text-blue-200 font-bold text-xl"
|
||||
>{$Locales.weekly_summary}</span
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
class="space-y-4 border border-dashed border-blue-400 rounded-lg p-4"
|
||||
>
|
||||
<div class="flex justify-between border-b border-gray-600 pb-2">
|
||||
<span>{$Locales.income}</span>
|
||||
<span class="text-blue-400">
|
||||
{#if $weeklyData.totalReceived !== undefined}
|
||||
{$weeklyData.totalReceived.toLocaleString($Currency.lang, {
|
||||
style: "currency",
|
||||
currency: $Currency.currency,
|
||||
minimumFractionDigits: 0,
|
||||
})}
|
||||
{:else}
|
||||
0
|
||||
{/if}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex justify-between border-b border-gray-600 pb-2">
|
||||
<span>{$Locales.expenses}</span>
|
||||
<span class="text-red-400">
|
||||
{#if $weeklyData.totalUsed !== undefined}
|
||||
{$weeklyData.totalUsed.toLocaleString($Currency.lang, {
|
||||
style: "currency",
|
||||
currency: $Currency.currency,
|
||||
minimumFractionDigits: 0,
|
||||
})}
|
||||
{:else}
|
||||
0
|
||||
{/if}
|
||||
</span>
|
||||
</div>
|
||||
<div class="mt-6">
|
||||
<div class="flex items-center mb-2">
|
||||
<i class="fa-duotone fa-chart-bar text-xl text-blue-400 mr-2"></i>
|
||||
<span>{$Locales.report}</span>
|
||||
</div>
|
||||
<div>
|
||||
<canvas bind:this={chartCanvas}></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Latest Transactions -->
|
||||
<div class="bg-gray-700 rounded-xl p-6 w-[380px] h-[400px] flex-none">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<div class="flex items-center">
|
||||
<i class="fa-duotone fa-file-invoice text-2xl text-blue-400 mr-2"
|
||||
></i>
|
||||
<span class="text-blue-200 font-bold text-xl"
|
||||
>{$Locales.latest_transactions}</span
|
||||
>
|
||||
</div>
|
||||
<div class="bg-gray-600 rounded-full px-2 py-1">
|
||||
<span class="text-white text-sm"
|
||||
>{bankData.transactions.length}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="space-y-3 border border-dashed border-blue-400 rounded-lg p-4 h-[310px]"
|
||||
>
|
||||
{#if bankData.transactions.length > 0}
|
||||
{#each bankData.transactions.slice(0, 5) as transaction}
|
||||
<div class="space-y-2">
|
||||
<div class="flex justify-between">
|
||||
<span class="truncate">{transaction.description}</span>
|
||||
<p
|
||||
class={`text-md font-bold ${transaction.isIncome ? "text-green-500" : "text-red-500"}`}
|
||||
>
|
||||
{transaction.isIncome ? "+" : "-"}
|
||||
{transaction.amount.toLocaleString($Currency.lang, {
|
||||
style: "currency",
|
||||
currency: $Currency.currency,
|
||||
minimumFractionDigits: 0,
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
<div class="border-b border-gray-600"></div>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
<div class="mt-6 flex justify-center">
|
||||
<button
|
||||
class="bg-blue-600/10 border border-blue-500 hover:bg-blue-800/50 text-white font-bold py-2 px-4 mt-4 duration-500 rounded-lg cursor-pointer"
|
||||
on:click={() => {
|
||||
showOverview.set(false);
|
||||
showBills.set(false);
|
||||
showHistory.set(true);
|
||||
showHeav.set(false);
|
||||
}}
|
||||
>
|
||||
{$Locales.see_all}
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="p-4 text-center text-blue-200">
|
||||
<i class="fa-duotone fa-check-circle text-2xl mb-2"></i>
|
||||
<p>{$Locales.no_transactions}</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-gray-700 rounded-xl p-4 w-[380px] h-[400px] flex-none">
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<div class="flex items-center">
|
||||
<i class="fa-duotone fa-file-exclamation text-xl text-blue-400 mr-2"
|
||||
></i>
|
||||
<span class="text-blue-200 font-bold text-lg"
|
||||
>{$Locales.unpaid_bills}</span
|
||||
>
|
||||
</div>
|
||||
<div class="bg-gray-600 rounded-full px-2 py-1">
|
||||
<span class="text-white text-sm">{$transactions.length}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="space-y-0 border border-dashed border-blue-400 p-1 rounded-lg overflow-auto mt-6 h-[310px]"
|
||||
>
|
||||
{#if $transactions.length > 0}
|
||||
{#each $transactions.slice(0, 2) as transaction (transaction.id)}
|
||||
{#if !transaction.isPaid}
|
||||
<div class="p-2 rounded-lg flex justify-between items-center">
|
||||
<div class="flex flex-col">
|
||||
<div class="flex items-center">
|
||||
<span class="font-semibold text-[#f1f5f9]"
|
||||
>{transaction.description} #{transaction.id}</span
|
||||
>
|
||||
</div>
|
||||
<div class="flex items-center mt-1">
|
||||
<span class="text-sm text-gray-400"
|
||||
>{transaction.type}</span
|
||||
>
|
||||
</div>
|
||||
<div class="flex items-center mt-1">
|
||||
<span class="text-xs text-gray-500"
|
||||
>{transaction.timeAgo}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right flex flex-col items-end">
|
||||
<span
|
||||
class={`text-lg ${transaction.isIncome ? "text-green-400" : "text-red-400"}`}
|
||||
>
|
||||
{transaction.isIncome ? "+" : "-"}
|
||||
{transaction.amount.toLocaleString($Currency.lang, {
|
||||
style: "currency",
|
||||
currency: $Currency.currency,
|
||||
minimumFractionDigits: 0,
|
||||
})}
|
||||
</span>
|
||||
<div class="flex items-center mt-0">
|
||||
<div class="text-sm text-gray-400">
|
||||
{transaction.date}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="border-b border-gray-600"></div>
|
||||
{/if}
|
||||
{/each}
|
||||
<div class="mb-4 space-y-2 text-center">
|
||||
<div class="mt-[70px] flex justify-center">
|
||||
<button
|
||||
class="bg-blue-600/10 border border-blue-500 hover:bg-blue-800/50 text-white font-bold py-2 px-4 duration-500 rounded-lg cursor-pointer"
|
||||
on:click={() => {
|
||||
showOverview.set(false);
|
||||
showBills.set(true);
|
||||
showHistory.set(false);
|
||||
showHeav.set(false);
|
||||
}}
|
||||
>
|
||||
{$Locales.see_all}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="p-4 text-center text-blue-200">
|
||||
<i class="fa-duotone fa-check-circle text-2xl mb-2"></i>
|
||||
<p>{$Locales.no_unpaid_bills}</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- MODALS -->
|
||||
{#if $showTransferModal}
|
||||
<!-- svelte-ignore a11y-label-has-associated-control -->
|
||||
<div
|
||||
class="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50"
|
||||
>
|
||||
<div
|
||||
class="p-8 bg-gray-800 rounded-lg shadow-lg w-96"
|
||||
in:scale={{ duration: 250, easing: quintOut }}
|
||||
out:scale={{ duration: 250, easing: quintOut }}
|
||||
>
|
||||
<div class="flex items-center mb-4">
|
||||
<i
|
||||
class="fa-duotone fa-arrow-right-arrow-left text-3xl text-blue-400 mr-3"
|
||||
></i>
|
||||
<h2 class="text-2xl text-blue-200 font-bold">
|
||||
{$Locales.transfer_money}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<!-- Payment Method Selection -->
|
||||
<div class="mb-6">
|
||||
<p class="capitalize font-semibold text-blue-200 mb-2">
|
||||
{$Locales.payment_method}
|
||||
</p>
|
||||
<div class="flex flex-col space-y-4">
|
||||
{#if phone}
|
||||
<label
|
||||
class="flex items-center cursor-pointer bg-gray-700/50 rounded-lg p-3 border border-gray-600/20 hover:border-blue-400 transition duration-300"
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="payment"
|
||||
value="phone"
|
||||
bind:group={$transferData.contactType}
|
||||
class="hidden peer"
|
||||
/>
|
||||
<i class="fa-duotone fa-phone text-lg text-blue-400 mr-3"></i>
|
||||
<span class="text-white font-bold">{$Locales.phone_number}</span
|
||||
>
|
||||
<div class="ml-auto hidden peer-checked:block">
|
||||
<i class="fa-duotone fa-check-circle text-blue-400"></i>
|
||||
</div>
|
||||
</label>
|
||||
{/if}
|
||||
<label
|
||||
class="flex items-center cursor-pointer bg-gray-700/50 rounded-lg p-3 border border-gray-600/20 hover:border-blue-400 transition duration-300"
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="payment"
|
||||
value="id"
|
||||
bind:group={$transferData.contactType}
|
||||
class="hidden peer"
|
||||
/>
|
||||
<i class="fa-duotone fa-id-badge text-lg text-blue-400 mr-3"></i>
|
||||
<span class="text-white font-bold">{$Locales.id}</span>
|
||||
<div class="ml-auto hidden peer-checked:block">
|
||||
<i class="fa-duotone fa-check-circle text-blue-400"></i>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ID or Phone Number Input -->
|
||||
{#if $transferData.contactType === "phone" || $transferData.contactType === "id"}
|
||||
<div class="mb-6">
|
||||
<label class="block text-gray-400 mb-2">
|
||||
<i class="fa-duotone fa-id-card text-blue-400 mr-2"></i>
|
||||
{#if phone}
|
||||
{$Locales.id_or_phone_number}
|
||||
{:else}
|
||||
{$Locales.id}
|
||||
{/if}
|
||||
</label>
|
||||
<div class="relative">
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
class="w-full p-3 bg-gray-700/50 text-white pr-10 border border-blue-200/10 rounded-lg focus:outline-none
|
||||
focus:border-blue-400/50 transition-colors duration-500"
|
||||
bind:value={$transferData.idOrPhone}
|
||||
/>
|
||||
<i
|
||||
class="fa-duotone fa-user absolute top-1/2 right-3 transform -translate-y-1/2 text-gray-400"
|
||||
></i>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Amount Input -->
|
||||
<div class="mb-6">
|
||||
<label class="block text-gray-400 mb-2">
|
||||
<i class="fa-duotone fa-money-bill-wave text-blue-400 mr-2"
|
||||
></i>{$Locales.amount}
|
||||
</label>
|
||||
<div class="relative">
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
class="w-full p-3 bg-gray-700/50 text-white pr-10 border border-blue-200/10 rounded-lg focus:outline-none
|
||||
focus:border-blue-400/50 transition-colors duration-500"
|
||||
bind:value={$transferData.amount}
|
||||
/>
|
||||
<i
|
||||
class="fa-duotone fa-dollar-sign absolute top-1/2 right-3 transform -translate-y-1/2 text-gray-400"
|
||||
></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="flex justify-between items-center mt-6">
|
||||
<button
|
||||
class="flex items-center bg-red-600 hover:bg-red-700 text-white py-2 px-4 rounded focus:outline-none"
|
||||
on:click={closeModal}
|
||||
>
|
||||
<i class="fa-duotone fa-times-circle mr-2"></i>{$Locales.cancel}
|
||||
</button>
|
||||
<button
|
||||
class="flex items-center bg-blue-600 hover:bg-blue-700 text-white py-2 px-4 rounded focus:outline-none"
|
||||
on:click={async () => {
|
||||
confirmTransfer(
|
||||
$transferData.idOrPhone,
|
||||
$transferData.amount,
|
||||
$transferData.contactType
|
||||
);
|
||||
}}
|
||||
>
|
||||
<i class="fa-duotone fa-check-circle mr-2"></i>{$Locales.confirm}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{#if $showSureModalBills}
|
||||
<div
|
||||
class="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50"
|
||||
>
|
||||
<div
|
||||
class="bg-gray-700 p-8 rounded-lg shadow-lg w-96"
|
||||
in:scale={{ duration: 250, easing: quintOut }}
|
||||
out:scale={{ duration: 250, easing: quintOut }}
|
||||
>
|
||||
<div class="flex items-center mb-4">
|
||||
<i class="fa-duotone fa-question-circle text-3xl text-blue-400 mr-3"
|
||||
></i>
|
||||
<h2 class="text-2xl text-blue-200 font-bold">
|
||||
{$Locales.are_you_sure}
|
||||
</h2>
|
||||
</div>
|
||||
<p class="text-gray-300 mb-6">
|
||||
{$Locales.confirm_pay_all_bills}
|
||||
</p>
|
||||
<div class="flex justify-between items-center">
|
||||
<button
|
||||
class="flex items-center bg-red-600 hover:bg-red-700 text-white py-2 px-4 rounded focus:outline-none"
|
||||
on:click={() => {
|
||||
showSureModalBills.set(false);
|
||||
}}
|
||||
>
|
||||
<i class="fa-duotone fa-times-circle mr-2"></i>{$Locales.cancel}
|
||||
</button>
|
||||
<button
|
||||
class="flex items-center bg-blue-600 hover:bg-blue-700 text-white py-2 px-4 rounded focus:outline-none"
|
||||
on:click={async () => {
|
||||
if ($transactions.length > 0) {
|
||||
await payAllBills();
|
||||
showSureModalBills.set(false);
|
||||
} else {
|
||||
showSureModalBills.set(false);
|
||||
Notify(
|
||||
$Locales.pay_all_bills_error,
|
||||
$Locales.error,
|
||||
"circle-exclamation"
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<i class="fa-duotone fa-check-circle mr-2"></i>{$Locales.confirm}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
228
resources/[tools]/ps-banking/web/src/components/Stats.svelte
Normal file
228
resources/[tools]/ps-banking/web/src/components/Stats.svelte
Normal file
|
@ -0,0 +1,228 @@
|
|||
<script lang="ts">
|
||||
import { writable } from "svelte/store";
|
||||
import { slide, scale } from "svelte/transition";
|
||||
import { quintOut } from "svelte/easing";
|
||||
import { onMount } from "svelte";
|
||||
import {
|
||||
Notify,
|
||||
currentCash,
|
||||
bankBalance,
|
||||
Locales,
|
||||
Currency,
|
||||
} from "../store/data";
|
||||
import { fetchNui } from "../utils/fetchNui";
|
||||
import {
|
||||
Chart,
|
||||
LineController,
|
||||
LineElement,
|
||||
PointElement,
|
||||
LinearScale,
|
||||
Title,
|
||||
CategoryScale,
|
||||
Tooltip,
|
||||
} from "chart.js";
|
||||
|
||||
let totalTransactions = writable(0);
|
||||
let totalAmount = writable(0);
|
||||
|
||||
let dataSheets = {
|
||||
Transactions: [],
|
||||
Dates: [],
|
||||
};
|
||||
|
||||
let chart: Chart<"line", never[], string>;
|
||||
|
||||
async function fetchTransactionStats() {
|
||||
try {
|
||||
const stats = await fetchNui("ps-banking:client:getTransactionStats", {});
|
||||
totalTransactions.set(stats.totalCount);
|
||||
totalAmount.set(stats.totalAmount);
|
||||
|
||||
const transactionData = stats.transactionData.map(
|
||||
(stat: { amount: any }) => stat.amount
|
||||
);
|
||||
const transactionDates = stats.transactionData.map(
|
||||
(stat: { date: any }) => new Date(stat.date).toLocaleDateString()
|
||||
);
|
||||
|
||||
dataSheets.Transactions = transactionData;
|
||||
dataSheets.Dates = transactionDates;
|
||||
|
||||
if (chart) {
|
||||
chart.data.labels = transactionDates;
|
||||
chart.data.datasets[0].data = transactionData;
|
||||
chart.update();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching transaction stats:", error);
|
||||
}
|
||||
}
|
||||
|
||||
Chart.register(
|
||||
LineController,
|
||||
LineElement,
|
||||
PointElement,
|
||||
LinearScale,
|
||||
Title,
|
||||
CategoryScale,
|
||||
Tooltip
|
||||
);
|
||||
|
||||
onMount(() => {
|
||||
fetchTransactionStats();
|
||||
|
||||
// @ts-expect-error
|
||||
const ctx = document.getElementById("transactionChart").getContext("2d");
|
||||
chart = new Chart(ctx, {
|
||||
type: "line",
|
||||
data: {
|
||||
labels: dataSheets.Dates,
|
||||
datasets: [
|
||||
{
|
||||
label: "Transactions",
|
||||
data: dataSheets.Transactions,
|
||||
borderColor: "rgba(59, 130, 246, 1)",
|
||||
backgroundColor: "rgba(59, 130, 246, 0.2)",
|
||||
fill: true,
|
||||
tension: 0.5,
|
||||
pointStyle: "rectRounded",
|
||||
pointRadius: 5,
|
||||
pointHoverRadius: 7,
|
||||
borderWidth: 2,
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true,
|
||||
position: "top",
|
||||
labels: {
|
||||
usePointStyle: true,
|
||||
color: "white",
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
backgroundColor: "rgba(0,0,0,0.7)",
|
||||
titleColor: "white",
|
||||
bodyColor: "white",
|
||||
cornerRadius: 4,
|
||||
displayColors: false,
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
display: true,
|
||||
title: {
|
||||
display: true,
|
||||
text: $Locales.date,
|
||||
color: "white",
|
||||
},
|
||||
ticks: {
|
||||
color: "white",
|
||||
},
|
||||
grid: {
|
||||
color: "rgba(255,255,255,0.1)",
|
||||
},
|
||||
},
|
||||
y: {
|
||||
display: true,
|
||||
title: {
|
||||
display: true,
|
||||
text: $Locales.amount,
|
||||
color: "white",
|
||||
},
|
||||
ticks: {
|
||||
color: "white",
|
||||
},
|
||||
grid: {
|
||||
color: "rgba(255,255,255,0.1)",
|
||||
},
|
||||
suggestedMin: 0,
|
||||
suggestedMax: dataSheets.Transactions.reduce(
|
||||
(acc, val) => acc + val,
|
||||
0
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-label-has-associated-control -->
|
||||
<div class="absolute w-full h-full bg-gray-800 text-white">
|
||||
<div
|
||||
class="absolute w-[90%] h-full p-6 overflow-auto left-[130px]"
|
||||
in:slide={{ duration: 1000, easing: quintOut }}
|
||||
>
|
||||
<div class="bg-gray-700/20 rounded-lg shadow-md p-6">
|
||||
<h2 class="text-3xl font-bold mb-6">{$Locales.statistics_reports}</h2>
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<div class="flex items-center">
|
||||
<i class="fa-duotone fa-chart-line text-3xl text-blue-200 mr-3"></i>
|
||||
<h3 class="text-2xl font-semibold text-blue-200">
|
||||
{$Locales.overview}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="bg-gray-500/40 rounded-xl px-3 py-1 flex items-center">
|
||||
<i class="fa-duotone fa-wallet text-gray-400 mr-2"></i>
|
||||
<span class="text-lg font-semibold text-white">
|
||||
{$Locales.total_balance}: {#if $bankBalance !== undefined}
|
||||
{$bankBalance.toLocaleString($Currency.lang, {
|
||||
style: "currency",
|
||||
currency: $Currency.currency,
|
||||
minimumFractionDigits: 0,
|
||||
})}
|
||||
{/if}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
|
||||
<div class="bg-gray-700 p-4 rounded-lg shadow">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h4 class="text-xl font-semibold text-blue-200">
|
||||
{$Locales.total_transactions}
|
||||
</h4>
|
||||
<i class="fa-duotone fa-exchange-alt text-yellow-400 text-2xl"></i>
|
||||
</div>
|
||||
<p class="text-2xl font-bold">{$totalTransactions}</p>
|
||||
</div>
|
||||
<div class="bg-gray-700 p-4 rounded-lg shadow">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h4 class="text-xl font-semibold text-blue-200">
|
||||
{$Locales.amount}
|
||||
</h4>
|
||||
<i class="fa-duotone fa-coins text-green-400 text-2xl"></i>
|
||||
</div>
|
||||
<p class="text-2xl font-bold">
|
||||
{#if $totalAmount !== undefined}
|
||||
{$totalAmount.toLocaleString($Currency.lang, {
|
||||
style: "currency",
|
||||
currency: $Currency.currency,
|
||||
minimumFractionDigits: 0,
|
||||
})}
|
||||
{:else}
|
||||
0
|
||||
{/if}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-gray-700 p-4 rounded-lg shadow">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h4 class="text-xl font-semibold text-blue-200">
|
||||
{$Locales.transactions_trend}
|
||||
</h4>
|
||||
<i class="fa-duotone fa-chart-line text-blue-400 text-2xl"></i>
|
||||
</div>
|
||||
<div
|
||||
class="relative w-[1000px] left-[15%]"
|
||||
in:scale={{ duration: 2500, easing: quintOut }}
|
||||
>
|
||||
<canvas id="transactionChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
3
resources/[tools]/ps-banking/web/src/global.d.ts
vendored
Normal file
3
resources/[tools]/ps-banking/web/src/global.d.ts
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
/// <reference types="@sveltejs/kit" />
|
||||
|
||||
declare module '*.csv';
|
8
resources/[tools]/ps-banking/web/src/main.ts
Normal file
8
resources/[tools]/ps-banking/web/src/main.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
import "./app.pcss";
|
||||
import App from "./App.svelte";
|
||||
|
||||
const app = new App({
|
||||
target: document.getElementById("app")!,
|
||||
});
|
||||
|
||||
export default app;
|
|
@ -0,0 +1,46 @@
|
|||
<script lang="ts">
|
||||
import { useNuiEvent } from "../utils/useNuiEvent";
|
||||
import { fetchNui } from "../utils/fetchNui";
|
||||
import { onMount } from "svelte";
|
||||
import { visibility } from "../store/stores";
|
||||
import ATM from "../components/ATM.svelte";
|
||||
import { showATM } from "../store/data";
|
||||
|
||||
let isVisible: boolean;
|
||||
|
||||
useNuiEvent<boolean>("openATM", () => {
|
||||
showATM.set(true);
|
||||
});
|
||||
|
||||
visibility.subscribe((visible) => {
|
||||
isVisible = visible;
|
||||
});
|
||||
|
||||
useNuiEvent<boolean>("openBank", () => {
|
||||
visibility.set(true);
|
||||
});
|
||||
useNuiEvent<boolean>("hideGarageMenu", () => {
|
||||
visibility.set(false);
|
||||
});
|
||||
onMount(() => {
|
||||
const keyHandler = (e: KeyboardEvent) => {
|
||||
if (isVisible && ["Escape"].includes(e.code)) {
|
||||
fetchNui("ps-banking:client:hideUI");
|
||||
visibility.set(false);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", keyHandler);
|
||||
|
||||
return () => window.removeEventListener("keydown", keyHandler);
|
||||
});
|
||||
</script>
|
||||
|
||||
<main>
|
||||
{#if isVisible}
|
||||
<slot />
|
||||
{/if}
|
||||
{#if $showATM}
|
||||
<ATM />
|
||||
{/if}
|
||||
</main>
|
250
resources/[tools]/ps-banking/web/src/store/data.ts
Normal file
250
resources/[tools]/ps-banking/web/src/store/data.ts
Normal file
|
@ -0,0 +1,250 @@
|
|||
import { writable, type Writable } from "svelte/store";
|
||||
|
||||
export interface Notification {
|
||||
id: number;
|
||||
message: string;
|
||||
title: string;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
export const notifications = writable<Notification[]>([]);
|
||||
let notificationId = 0;
|
||||
|
||||
export function Notify(message: string, title: string, icon: string) {
|
||||
notificationId += 1;
|
||||
const newNotification: Notification = {
|
||||
id: notificationId,
|
||||
message,
|
||||
title,
|
||||
icon,
|
||||
};
|
||||
notifications.update((n) => [...n, newNotification]);
|
||||
setTimeout(() => {
|
||||
notifications.update((n) =>
|
||||
n.filter((notification) => notification.id !== newNotification.id)
|
||||
);
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
export const showOverview = writable(true);
|
||||
export const showBills = writable(false);
|
||||
export const showHistory = writable(false);
|
||||
export const showHeav = writable(false);
|
||||
export const showIndseat = writable(false);
|
||||
export const showStats = writable(false);
|
||||
export const showAccounts = writable(false);
|
||||
export const showATM = writable(false);
|
||||
export const currentCash = writable(500);
|
||||
export const bankBalance = writable(10000);
|
||||
|
||||
export const Currency = writable({
|
||||
lang: "en", // da-DK
|
||||
currency: "USD", // DKK
|
||||
});
|
||||
|
||||
export const Locales = writable({
|
||||
atm: "ATM",
|
||||
cash: "Cash",
|
||||
bank_balance: "Bank Balance",
|
||||
deposit_amount: "Deposit Amount",
|
||||
withdraw_amount: "Withdraw Amount",
|
||||
submit: "Submit",
|
||||
close: "Close",
|
||||
overview: "Overview",
|
||||
bills: "Bills",
|
||||
history: "History",
|
||||
withdraw: "Withdraw",
|
||||
deposit: "Deposit",
|
||||
stats: "Stats",
|
||||
transactions: "Transactions",
|
||||
total: "Total",
|
||||
search_transactions: "Search transactions...",
|
||||
description: "Description",
|
||||
type: "Type",
|
||||
time_ago: "Time Ago",
|
||||
amount: "Amount",
|
||||
date: "Date",
|
||||
pay_invoice: "Pay Invoice",
|
||||
payment_completed: "Payment Completed",
|
||||
from: "From",
|
||||
delete_all_transactions: "Delete All Transactions",
|
||||
are_you_sure: "Are you sure?",
|
||||
delete_confirmation:
|
||||
"Are you sure you want to delete all your transactions? (Only do this if the menu lags!)",
|
||||
cancel: "Cancel",
|
||||
confirm: "Confirm",
|
||||
history_empty: "Your history is empty",
|
||||
all_history_deleted: "You have deleted all your history",
|
||||
error: "Error",
|
||||
success: "Success",
|
||||
new_cash: "New Cash",
|
||||
withdraw_success: "Withdrawal Successful",
|
||||
withdraw_error: "Your bank account does not have enough funds",
|
||||
withdraw_button: "WITHDRAW",
|
||||
new_bank: "New Bank Balance",
|
||||
current_cash: "Current Cash",
|
||||
deposit_success: "Deposit Successful",
|
||||
deposit_error: "You do not have enough cash",
|
||||
deposit_button: "DEPOSIT",
|
||||
total_balance: "Total Balance",
|
||||
quick_actions: "Quick Actions",
|
||||
transfer_money: "Transfer Money",
|
||||
easy_transfer: "Easily transfer money to people",
|
||||
transfer: "Transfer",
|
||||
pay_bills: "Pay Bills",
|
||||
pay_pending_bills: "Quickly pay your pending bills",
|
||||
pay: "Pay",
|
||||
withdraw_all_money: "Withdraw All Money",
|
||||
withdraw_all_from_account: "Withdraw all your money from your account",
|
||||
deposit_cash: "Deposit Cash",
|
||||
deposit_all_cash: "Deposit all your cash into your account",
|
||||
weekly_summary: "Weekly Summary",
|
||||
income: "Income",
|
||||
expenses: "Expenses",
|
||||
report: "Report",
|
||||
latest_transactions: "Latest Transactions",
|
||||
see_all: "SEE ALL",
|
||||
unpaid_bills: "Unpaid Invoices",
|
||||
no_unpaid_bills: "No unpaid invoices",
|
||||
confirm_pay_all_bills: "Are you sure you want to pay all your bills?",
|
||||
pay_all_bills: "Pay All Bills",
|
||||
pay_all_bills_success: "You have paid all your bills!",
|
||||
pay_all_bills_error: "You have no bills",
|
||||
payment_method: "Payment Method",
|
||||
phone_number: "Phone Number",
|
||||
id: "ID",
|
||||
id_or_phone_number: "ID or Phone Number",
|
||||
no_cash_on_you: "You have no cash on you",
|
||||
deposit_all_success: "All your cash has been deposited",
|
||||
no_money_on_account: "Your account is empty",
|
||||
withdraw_all_success: "You have withdrawn all your money from the account",
|
||||
invoices: "Invoices",
|
||||
statistics_reports: "Statistics and Reports",
|
||||
balance_trend: "Balance Trend",
|
||||
balance: "Balance",
|
||||
used: "Used",
|
||||
month: "Month",
|
||||
balance_dkk: "Balance",
|
||||
withdrawn: "You have withdrawn",
|
||||
deposited: "You have deposited",
|
||||
no_transactions: "No recent transactions",
|
||||
transactions_trend: "Transactions Trend",
|
||||
total_transactions: "Total Transactions",
|
||||
accounts: "Accounts",
|
||||
account_number_copied: "Account number copied to clipboard",
|
||||
new_user_to_account: "New user to account",
|
||||
server_id: "Server ID",
|
||||
add_user: "Add User",
|
||||
new_account_name: "New Account Name",
|
||||
new_name: "New Name",
|
||||
rename: "Rename",
|
||||
create_new_account: "Create New Account",
|
||||
account_holder: "Account Holder",
|
||||
initial_balance: "Initial Balance",
|
||||
create: "Create",
|
||||
delete_account: "Delete Account",
|
||||
are_you_sure_you_want_to_delete_this_account:
|
||||
"Are you sure you want to delete this account?",
|
||||
delete: "Delete",
|
||||
remove_user_from_account: "Remove User from Account",
|
||||
select_user: "Select User",
|
||||
remove: "Remove",
|
||||
withdraw_from_account: "Withdraw from Account",
|
||||
deposit_to_account: "Deposit to Account",
|
||||
removed_successfully: "removed Successfully",
|
||||
select_account_and_user: "Please select an account and a user",
|
||||
account_deleted_successfully: "Account deleted successfully",
|
||||
new_account_created_successfully: "New account created successfully",
|
||||
withdrew: "Withdrew",
|
||||
successfully: "Successfully",
|
||||
select_valid_account_and_amount: "Please select a valid account and amount",
|
||||
openBank: "Access Bank",
|
||||
openATM: "Access ATM",
|
||||
account_deletion_failed: "Account deletion failed",
|
||||
withdrawal_failed: "Withdrawal failed",
|
||||
deposit_failed: "Deposit failed",
|
||||
user_added_successfully: "added successfully",
|
||||
user_addition_failed: "Failed to add user",
|
||||
new_account_creation_failed: "Failed to create new account",
|
||||
account_renamed_successfully: "Account renamed successfully",
|
||||
account_rename_failed: "Account rename failed",
|
||||
rename_account: "Change name",
|
||||
});
|
||||
|
||||
export const Transactions: Writable<Array<any>> = writable([
|
||||
// {
|
||||
// id: 8,
|
||||
// description: "Åbnede en ny konto",
|
||||
// type: "Fra konto",
|
||||
// amount: 1000,
|
||||
// date: "2022/08/13",
|
||||
// timeAgo: "For 18 timer siden",
|
||||
// isIncome: false,
|
||||
// },
|
||||
// {
|
||||
// id: 7,
|
||||
// description: "Indsatte 500 DKK på konto",
|
||||
// type: "Til konto",
|
||||
// amount: 500,
|
||||
// date: "2022/08/13",
|
||||
// timeAgo: "For 18 timer siden",
|
||||
// isIncome: true,
|
||||
// },
|
||||
// {
|
||||
// id: 6,
|
||||
// description: "Indsatte 500 DKK på konto",
|
||||
// type: "Til konto",
|
||||
// amount: 500,
|
||||
// date: "2022/08/13",
|
||||
// timeAgo: "For 18 timer siden",
|
||||
// isIncome: true,
|
||||
// },
|
||||
// {
|
||||
// id: 5,
|
||||
// description: "Hævede 500 DKK fra en hæveautomat",
|
||||
// type: "Fra konto",
|
||||
// amount: -500,
|
||||
// date: "2022/08/13",
|
||||
// timeAgo: "For 18 timer siden",
|
||||
// isIncome: false,
|
||||
// },
|
||||
// {
|
||||
// id: 4,
|
||||
// description: "Indsatte 500 DKK på konto",
|
||||
// type: "Til konto",
|
||||
// amount: 500,
|
||||
// date: "2022/08/13",
|
||||
// timeAgo: "For 18 timer siden",
|
||||
// isIncome: true,
|
||||
// },
|
||||
]);
|
||||
|
||||
export const Bills: Writable<Array<any>> = writable([
|
||||
// {
|
||||
// id: 1,
|
||||
// description: "Mekaniker Regning",
|
||||
// type: "Auto Exotic",
|
||||
// amount: 1000,
|
||||
// date: "2022/08/13",
|
||||
// timeAgo: "For 18 timer siden",
|
||||
// isPaid: false,
|
||||
// },
|
||||
// {
|
||||
// id: 2,
|
||||
// description: "Mekaniker Regning",
|
||||
// type: "Auto Exotic",
|
||||
// amount: 1000,
|
||||
// date: "2022/08/13",
|
||||
// timeAgo: "For 18 timer siden",
|
||||
// isPaid: false,
|
||||
// },
|
||||
// {
|
||||
// id: 3,
|
||||
// description: "Mekaniker Regning",
|
||||
// type: "Auto Exotic",
|
||||
// amount: 1000,
|
||||
// date: "2022/08/13",
|
||||
// timeAgo: "For 18 timer siden",
|
||||
// isPaid: false,
|
||||
// },
|
||||
]);
|
3
resources/[tools]/ps-banking/web/src/store/stores.ts
Normal file
3
resources/[tools]/ps-banking/web/src/store/stores.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
import { writable } from "svelte/store";
|
||||
|
||||
export const visibility = writable(false);
|
30
resources/[tools]/ps-banking/web/src/utils/debugData.ts
Normal file
30
resources/[tools]/ps-banking/web/src/utils/debugData.ts
Normal file
|
@ -0,0 +1,30 @@
|
|||
import {isEnvBrowser} from "./misc";
|
||||
|
||||
interface DebugEvent<T = any> {
|
||||
action: string;
|
||||
data: T;
|
||||
}
|
||||
|
||||
/**
|
||||
* Emulates dispatching an event using SendNuiMessage in the lua scripts.
|
||||
* This is used when developing in browser
|
||||
*
|
||||
* @param events - The event you want to cover
|
||||
* @param timer - How long until it should trigger (ms)
|
||||
*/
|
||||
export const debugData = <P>(events: DebugEvent<P>[], timer = 1000): void => {
|
||||
if (isEnvBrowser()) {
|
||||
for (const event of events) {
|
||||
setTimeout(() => {
|
||||
window.dispatchEvent(
|
||||
new MessageEvent("message", {
|
||||
data: {
|
||||
action: event.action,
|
||||
data: event.data,
|
||||
},
|
||||
})
|
||||
);
|
||||
}, timer);
|
||||
}
|
||||
}
|
||||
};
|
27
resources/[tools]/ps-banking/web/src/utils/fetchNui.ts
Normal file
27
resources/[tools]/ps-banking/web/src/utils/fetchNui.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
/**
|
||||
* @param eventName - The endpoint eventname to target
|
||||
* @param data - Data you wish to send in the NUI Callback
|
||||
*
|
||||
* @return returnData - A promise for the data sent back by the NuiCallbacks CB argument
|
||||
*/
|
||||
|
||||
export async function fetchNui<T = any>(
|
||||
eventName: string,
|
||||
data: unknown = {}
|
||||
): Promise<T> {
|
||||
const options = {
|
||||
method: "post",
|
||||
headers: {
|
||||
"Content-Type": "application/json; charset=UTF-8",
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
};
|
||||
|
||||
const resourceName = (window as any).GetParentResourceName
|
||||
? (window as any).GetParentResourceName()
|
||||
: "nui-frame-app";
|
||||
|
||||
const resp = await fetch(`https://${resourceName}/${eventName}`, options);
|
||||
|
||||
return await resp.json();
|
||||
}
|
1
resources/[tools]/ps-banking/web/src/utils/misc.ts
Normal file
1
resources/[tools]/ps-banking/web/src/utils/misc.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export const isEnvBrowser = (): boolean => !(window as any).invokeNative;
|
51
resources/[tools]/ps-banking/web/src/utils/useNuiEvent.ts
Normal file
51
resources/[tools]/ps-banking/web/src/utils/useNuiEvent.ts
Normal file
|
@ -0,0 +1,51 @@
|
|||
import { onDestroy } from "svelte";
|
||||
|
||||
interface NuiMessage<T = unknown> {
|
||||
action: string;
|
||||
data: T;
|
||||
}
|
||||
|
||||
/**
|
||||
* A function that manage events listeners for receiving data from the client scripts
|
||||
* @param action The specific `action` that should be listened for.
|
||||
* @param handler The callback function that will handle data relayed by this function
|
||||
*
|
||||
* @example
|
||||
* useNuiEvent<{visibility: true, wasVisible: 'something'}>('setVisible', (data) => {
|
||||
* // whatever logic you want
|
||||
* })
|
||||
*
|
||||
**/
|
||||
|
||||
type NuiEventHandler<T = any> = (data: T) => void;
|
||||
|
||||
const eventListeners = new Map<string, NuiEventHandler[]>();
|
||||
|
||||
const eventListener = (event: MessageEvent<NuiMessage>) => {
|
||||
const { action, data } = event.data;
|
||||
const handlers = eventListeners.get(action);
|
||||
|
||||
if (handlers) {
|
||||
handlers.forEach((handler) => handler(data));
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("message", eventListener);
|
||||
|
||||
export function useNuiEvent<T = unknown>(
|
||||
action: string,
|
||||
handler: NuiEventHandler<T>
|
||||
) {
|
||||
const handlers = eventListeners.get(action) || [];
|
||||
handlers.push(handler);
|
||||
eventListeners.set(action, handlers);
|
||||
|
||||
onDestroy(() => {
|
||||
const handlers = eventListeners.get(action) || [];
|
||||
|
||||
eventListeners.set(
|
||||
action,
|
||||
handlers.filter((h) => h !== handler)
|
||||
);
|
||||
});
|
||||
}
|
2
resources/[tools]/ps-banking/web/src/vite-env.d.ts
vendored
Normal file
2
resources/[tools]/ps-banking/web/src/vite-env.d.ts
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
/// <reference types="svelte" />
|
||||
/// <reference types="vite/client" />
|
Loading…
Add table
Add a link
Reference in a new issue