@pro-acc/pos-core 0.0.0-stage → 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -2
- package/app/app.vue +29 -0
- package/app/layouts/pos.vue +14 -0
- package/app/middleware/auth.global.ts +43 -0
- package/app/pages/archive.vue +327 -0
- package/app/pages/login.vue +58 -0
- package/app/pages/sessions/index.vue +198 -0
- package/app/pages/settings.vue +381 -0
- package/app/repository/factory.ts +35 -0
- package/app/repository/modules/auth.ts +26 -0
- package/app/repository/modules/bankTerminal.ts +11 -0
- package/app/repository/modules/order.ts +15 -0
- package/app/repository/modules/product.ts +13 -0
- package/app/repository/modules/session.ts +37 -0
- package/app/repository/modules/settings.ts +14 -0
- package/app/repository/modules/sync.ts +36 -0
- package/env.d.ts +5 -0
- package/i18n/locales/en.json +289 -0
- package/i18n/locales/sq.json +291 -0
- package/i18n/locales/tr.json +291 -0
- package/nuxt.config.ts +98 -0
- package/package.json +205 -4
- package/security/app/error.vue +4 -0
- package/security/nuxt.config.ts +15 -0
- package/security/server/api/system/update-check.get.ts +9 -0
- package/security/server/middleware/10-security-headers.ts +45 -0
- package/security/server/middleware/11-csrf-origin.ts +45 -0
- package/security/server/utils/rateLimit.ts +89 -0
- package/security/server/utils/update-check.ts +93 -0
- package/server/api/auth/change-password.post.ts +27 -0
- package/server/api/auth/login.post.ts +53 -0
- package/server/api/auth/logout.post.ts +7 -0
- package/server/api/auth/me.get.ts +15 -0
- package/server/api/bank-terminals/[id].delete.ts +8 -0
- package/server/api/bank-terminals/[id].put.ts +10 -0
- package/server/api/bank-terminals/index.get.ts +6 -0
- package/server/api/bank-terminals/index.post.ts +9 -0
- package/server/api/customers/index.get.ts +7 -0
- package/server/api/orders/index.get.ts +6 -0
- package/server/api/orders/index.post.ts +10 -0
- package/server/api/products/[id].get.ts +10 -0
- package/server/api/products/by-code/[code].get.ts +10 -0
- package/server/api/products/index.get.ts +6 -0
- package/server/api/seed.post.ts +40 -0
- package/server/api/session/close.post.ts +13 -0
- package/server/api/session/current.get.ts +6 -0
- package/server/api/session/open.post.ts +6 -0
- package/server/api/sessions/[id]/orders.get.ts +16 -0
- package/server/api/sessions/[id].get.ts +10 -0
- package/server/api/sessions/index.get.ts +6 -0
- package/server/api/settings/index.get.ts +6 -0
- package/server/api/settings/index.put.ts +9 -0
- package/server/api/sync/accounts.get.ts +6 -0
- package/server/api/sync/all.post.ts +22 -0
- package/server/api/sync/bootstrap.post.ts +6 -0
- package/server/api/sync/companies.get.ts +6 -0
- package/server/api/sync/gl-accounts.get.ts +6 -0
- package/server/api/sync/health.get.ts +6 -0
- package/server/api/sync/products.post.ts +6 -0
- package/server/api/sync/session.post.ts +8 -0
- package/server/api/sync/test.post.ts +19 -0
- package/server/api/sync/users.post.ts +4 -0
- package/server/api/sync/warehouses.get.ts +6 -0
- package/server/middleware/00-sw-noop.ts +12 -0
- package/server/middleware/01-pos-auth.ts +37 -0
- package/server/plugins/00-env-check.ts +14 -0
- package/server/repository/base.repository.ts +52 -0
- package/server/service/auth.base.ts +74 -0
- package/server/service/bank-terminal.base.ts +33 -0
- package/server/service/customer.base.ts +51 -0
- package/server/service/order.base.ts +185 -0
- package/server/service/product.base.ts +43 -0
- package/server/service/session.base.ts +169 -0
- package/server/service/settings.base.ts +83 -0
- package/server/service/sync.base.ts +368 -0
- package/server/shared/fiscalPrinter.ts +221 -0
- package/server/utils/apiHandler.ts +32 -0
- package/server/utils/crudHandlers.ts +97 -0
- package/server/utils/customer.ts +42 -0
- package/server/utils/decimal.ts +9 -0
- package/server/utils/orderMath.ts +21 -0
- package/server/utils/posAuth.ts +64 -0
- package/server/utils/sessionPayload.ts +87 -0
- package/server/utils/tender.ts +78 -0
- package/server/utils/terminalSession.ts +63 -0
- package/server/validation/bank-terminal.validation.ts +15 -0
- package/server/validation/order.validation.ts +76 -0
- package/server/validation/product.validation.ts +29 -0
- package/server/validation/settings.validation.ts +23 -0
package/README.md
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @pro-acc/pos-core
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Pro-Acc POS/Restoran ortak Nuxt 4 katmanı. UI uygulaması ve veritabanı bu paketten bağımsızdır; tüketici kendi Prisma istemcisini, server/service sınıflarını ve API plugin'ini sağlar.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
pnpm add @pro-acc/ui@0.0.2 @pro-acc/pos-core@0.0.1
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
POS/Restoran Nuxt yapılandırması extends: ["@pro-acc/pos-core"] kullanır. Bu katman @pro-acc/ui modülünü pos:true ile kaydeder. Paylaşılan route'lar hizmetleri tüketici kökü üzerinden (~~/server/service) çağırır; uygulama override davranışı korunur.
|
|
10
|
+
|
|
11
|
+
Yalnız HTTP güvenliği ve güncelleme kontrolü isteyen uygulama extends: ["@pro-acc/pos-core/security"] kullanabilir; bu alt katman POS API'larını kurmaz.
|
|
12
|
+
|
|
13
|
+
UI bileşenleri/store'ları yalnız @pro-acc/ui paketinde tanımlıdır. Bu paket ortak sayfalar, repository modülleri ve Nitro servis/API kodunu tutar. Hesap/DB anahtarları tüketici runtimeConfig ile sağlanır.
|
|
14
|
+
|
|
15
|
+
[GitHub kaynakları](https://github.com/olcan123/pro-acc-packages/tree/main/packages/pos-core) · [npm](https://www.npmjs.com/package/@pro-acc/pos-core)
|
package/app/app.vue
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<!-- app.vue — POS/Restoran ortak kök bileşen (pos-core layer): NuxtLayout + NuxtPage + global onay diyaloğu.
|
|
2
|
+
Uygulama adı app.config.ts → appTitle ile belirlenir (pos: "Pro-Acc POS", restoran: "Pro-Acc Restoran"). -->
|
|
3
|
+
<template>
|
|
4
|
+
<NuxtLayout>
|
|
5
|
+
<NuxtPage />
|
|
6
|
+
<!-- @pro-acc/pos-ui katmanından auto-import (FormFieldXxx) -->
|
|
7
|
+
<FormFieldConfirmDialog />
|
|
8
|
+
</NuxtLayout>
|
|
9
|
+
</template>
|
|
10
|
+
|
|
11
|
+
<script setup>
|
|
12
|
+
const appConfig = useAppConfig();
|
|
13
|
+
const appTitle = appConfig.appTitle || "Pro-Acc POS";
|
|
14
|
+
useHead({
|
|
15
|
+
titleTemplate: (title) => (title ? `${title} | ${appTitle}` : appTitle),
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
// Dev modda eski PWA service worker'larını otomatik sil.
|
|
19
|
+
// Tarayıcıda kalan workbox SW'lar Vite HMR ile çakışıp "Failed to fetch dynamically
|
|
20
|
+
// imported module" hatasına yol açar. Production'da SW Nuxt PWA modülü tarafından
|
|
21
|
+
// doğru şekilde yönetildiğinden buradaki kod çalışmaz.
|
|
22
|
+
if (import.meta.client && import.meta.dev) {
|
|
23
|
+
navigator.serviceWorker?.getRegistrations().then((registrations) => {
|
|
24
|
+
for (const reg of registrations) {
|
|
25
|
+
reg.unregister();
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
</script>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<!-- layouts/pos.vue — Tam ekran satış ekranı kabuğu: Topbar + Subbar + içerik. -->
|
|
2
|
+
<template>
|
|
3
|
+
<div class="h-screen flex flex-col bg-gray-100 dark:bg-gray-950 overflow-hidden">
|
|
4
|
+
<LayoutPosTopbar />
|
|
5
|
+
<LayoutPosSubbar />
|
|
6
|
+
<div class="flex-1 min-h-0">
|
|
7
|
+
<slot />
|
|
8
|
+
</div>
|
|
9
|
+
</div>
|
|
10
|
+
</template>
|
|
11
|
+
|
|
12
|
+
<script setup>
|
|
13
|
+
// Satış ekranı için tam ekran düzen (sidebar yok).
|
|
14
|
+
</script>
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// auth.global.ts — POS/Restoran ortak global rota koruması (pos-core layer): oturum yoksa /login'e yönlendirir.
|
|
2
|
+
// Oturum httpOnly çerezde tutulduğundan kontrol istemci tarafında yapılır (SSR cookie
|
|
3
|
+
// forwarding karmaşası olmadan). /login sayfası muaftır.
|
|
4
|
+
// AUTH_ENABLED=false (dev modu) ise tüm auth kontrolü atlanır; mock kullanıcı kullanılır.
|
|
5
|
+
import { useAuthStore } from "@pro-acc/ui/pos/stores/auth.store.js";
|
|
6
|
+
|
|
7
|
+
// Giriş gerektirmeyen sayfalar: /login ve /settings (ilk kurulumda main bağlantısını
|
|
8
|
+
// yapılandırabilmek için — aksi halde "giriş için sync gerek, sync için ayar gerek" kilidi oluşur).
|
|
9
|
+
const PUBLIC_PATHS = ["/login", "/settings"];
|
|
10
|
+
|
|
11
|
+
// Dev mod için mock kullanıcı — main'deki getDevMockUser ile uyumlu
|
|
12
|
+
const DEV_MOCK_USER = {
|
|
13
|
+
userId: 1,
|
|
14
|
+
firstName: "Dev",
|
|
15
|
+
lastName: "User",
|
|
16
|
+
email: "developer@pro-acc.local",
|
|
17
|
+
role: "ADMIN",
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export default defineNuxtRouteMiddleware(async (to) => {
|
|
21
|
+
if (import.meta.server) return;
|
|
22
|
+
|
|
23
|
+
const config = useRuntimeConfig();
|
|
24
|
+
|
|
25
|
+
// Auth kapalıysa (dev modu) mock kullanıcı ile bypass et
|
|
26
|
+
if (!config.public.authEnabled) {
|
|
27
|
+
const auth = useAuthStore();
|
|
28
|
+
if (!auth.isInitialized) {
|
|
29
|
+
auth.user = DEV_MOCK_USER;
|
|
30
|
+
auth.isInitialized = true;
|
|
31
|
+
}
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (PUBLIC_PATHS.includes(to.path)) return;
|
|
36
|
+
|
|
37
|
+
const auth = useAuthStore();
|
|
38
|
+
if (!auth.isInitialized) await auth.fetchMe();
|
|
39
|
+
|
|
40
|
+
if (!auth.isAuthenticated) {
|
|
41
|
+
return navigateTo({ path: "/login", query: { redirect: to.fullPath } });
|
|
42
|
+
}
|
|
43
|
+
});
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
<!-- pages/archive.vue — POS Arşivi: kapalı vardiyalar ve fiş detayları. -->
|
|
2
|
+
<template>
|
|
3
|
+
<div class="max-w-5xl mx-auto flex flex-col gap-5">
|
|
4
|
+
<div class="flex items-center justify-between">
|
|
5
|
+
<h1 class="text-xl font-bold text-paper-900 dark:text-paper-100">
|
|
6
|
+
<i class="pi pi-box mr-2 text-indigo-500" />{{ $t("archive") }}
|
|
7
|
+
</h1>
|
|
8
|
+
<NuxtLink to="/" class="btn-base btn-secondary btn-size-sm rounded-lg">
|
|
9
|
+
<i class="pi pi-arrow-left mr-1" />{{ $t("back") }}
|
|
10
|
+
</NuxtLink>
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
<!-- Yükleniyor -->
|
|
14
|
+
<div v-if="loading" class="flex justify-center py-16">
|
|
15
|
+
<i class="pi pi-spin pi-spinner text-3xl text-blue-500" />
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
<template v-else>
|
|
19
|
+
<!-- Kapanmış vardiya yok -->
|
|
20
|
+
<div v-if="!closedSessions.length" class="card-panel p-10 text-center text-paper-500 dark:text-paper-400">
|
|
21
|
+
<i class="pi pi-inbox text-4xl mb-3 block" />
|
|
22
|
+
<p>{{ $t("no_closed_sessions") }}</p>
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
<div v-else class="flex flex-col gap-4">
|
|
26
|
+
<!-- Vardiya tablosu -->
|
|
27
|
+
<section class="card-panel overflow-hidden">
|
|
28
|
+
<table class="w-full text-sm">
|
|
29
|
+
<thead class="bg-paper-100 dark:bg-paper-800/60 text-paper-600 dark:text-paper-400">
|
|
30
|
+
<tr>
|
|
31
|
+
<th class="text-left px-4 py-2">{{ $t("z_report_no") }}</th>
|
|
32
|
+
<th class="text-left px-4 py-2">{{ $t("opened_at") }}</th>
|
|
33
|
+
<th class="text-left px-4 py-2">{{ $t("closed_at") }}</th>
|
|
34
|
+
<th class="text-right px-4 py-2">{{ $t("turnover") }}</th>
|
|
35
|
+
<th class="text-right px-4 py-2">{{ $t("total_cash") }}</th>
|
|
36
|
+
<th class="text-right px-4 py-2">{{ $t("total_card") }}</th>
|
|
37
|
+
<th class="text-center px-4 py-2">{{ $t("synced") }}</th>
|
|
38
|
+
<th class="px-4 py-2" />
|
|
39
|
+
</tr>
|
|
40
|
+
</thead>
|
|
41
|
+
<tbody>
|
|
42
|
+
<tr
|
|
43
|
+
v-for="s in closedSessions"
|
|
44
|
+
:key="s.id"
|
|
45
|
+
class="border-t border-paper-300 dark:border-paper-800 cursor-pointer transition-colors"
|
|
46
|
+
:class="
|
|
47
|
+
selectedSession?.id === s.id
|
|
48
|
+
? 'bg-indigo-50 dark:bg-indigo-950/30'
|
|
49
|
+
: 'hover:bg-gray-50 dark:hover:bg-gray-800/40'
|
|
50
|
+
"
|
|
51
|
+
@click="selectSession(s)"
|
|
52
|
+
>
|
|
53
|
+
<td class="px-4 py-2 font-semibold text-indigo-600 dark:text-indigo-400">
|
|
54
|
+
<i class="pi pi-receipt mr-1 text-xs" />{{ s.zReportNo }}
|
|
55
|
+
</td>
|
|
56
|
+
<td class="px-4 py-2 text-paper-600 dark:text-paper-400">{{ fmtDate(s.openedAt) }}</td>
|
|
57
|
+
<td class="px-4 py-2 text-paper-600 dark:text-paper-400">{{ fmtDate(s.closedAt) }}</td>
|
|
58
|
+
<td class="px-4 py-2 text-right tabular-nums font-semibold">{{ fmtCur(s.totalAmount) }}</td>
|
|
59
|
+
<td class="px-4 py-2 text-right tabular-nums text-green-700 dark:text-green-400">
|
|
60
|
+
{{ fmtCur(s.totalCash) }}
|
|
61
|
+
</td>
|
|
62
|
+
<td class="px-4 py-2 text-right tabular-nums text-blue-600 dark:text-blue-400">
|
|
63
|
+
{{ fmtCur(s.totalCard) }}
|
|
64
|
+
</td>
|
|
65
|
+
<td class="px-4 py-2 text-center" @click.stop>
|
|
66
|
+
<span v-if="sessionSyncStatus(s) === 'synced'" class="badge-base badge-success badge-size-sm">{{
|
|
67
|
+
$t("synced")
|
|
68
|
+
}}</span>
|
|
69
|
+
<span v-else class="flex items-center justify-center gap-1.5">
|
|
70
|
+
<span v-if="sessionSyncStatus(s) === 'pending'" class="badge-base badge-warning badge-size-sm">{{
|
|
71
|
+
$t("sync_pending")
|
|
72
|
+
}}</span>
|
|
73
|
+
<span v-else class="badge-base badge-danger badge-size-sm" :title="s.syncError || ''">{{
|
|
74
|
+
$t("not_synced")
|
|
75
|
+
}}</span>
|
|
76
|
+
<button
|
|
77
|
+
class="btn-base btn-secondary btn-size-xs rounded-md flex items-center gap-1"
|
|
78
|
+
:disabled="resyncingId === s.id"
|
|
79
|
+
@click.stop="resyncSession(s)"
|
|
80
|
+
>
|
|
81
|
+
<i :class="resyncingId === s.id ? 'pi pi-spin pi-spinner' : 'pi pi-refresh'" class="text-xs" />
|
|
82
|
+
{{ $t("resync") }}
|
|
83
|
+
</button>
|
|
84
|
+
</span>
|
|
85
|
+
</td>
|
|
86
|
+
<td class="px-4 py-2 text-center text-xs text-indigo-500">
|
|
87
|
+
<i class="pi pi-chevron-right" />
|
|
88
|
+
</td>
|
|
89
|
+
</tr>
|
|
90
|
+
</tbody>
|
|
91
|
+
</table>
|
|
92
|
+
</section>
|
|
93
|
+
|
|
94
|
+
<!-- Seçili vardiya — fiş detayları -->
|
|
95
|
+
<section v-if="selectedSession" class="card-panel overflow-hidden">
|
|
96
|
+
<!-- Başlık -->
|
|
97
|
+
<div class="flex items-center justify-between px-5 py-3 bg-indigo-600 text-white">
|
|
98
|
+
<div class="font-bold">
|
|
99
|
+
<i class="pi pi-receipt mr-2" />
|
|
100
|
+
{{ $t("z_report_no") }} {{ selectedSession.zReportNo }}
|
|
101
|
+
<span class="text-indigo-200 font-normal text-sm ml-2">
|
|
102
|
+
— {{ fmtDate(selectedSession.openedAt) }} → {{ fmtDate(selectedSession.closedAt) }}
|
|
103
|
+
</span>
|
|
104
|
+
</div>
|
|
105
|
+
<div class="flex items-center gap-4 text-sm">
|
|
106
|
+
<span
|
|
107
|
+
>{{ $t("turnover") }}:
|
|
108
|
+
<strong class="tabular-nums">{{ fmtCur(selectedSession.totalAmount) }}</strong></span
|
|
109
|
+
>
|
|
110
|
+
<span
|
|
111
|
+
>{{ $t("total_vat") }}:
|
|
112
|
+
<strong class="tabular-nums">{{ fmtCur(selectedSession.totalVat) }}</strong></span
|
|
113
|
+
>
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
<!-- Yükleniyor -->
|
|
118
|
+
<div v-if="ordersLoading" class="flex justify-center py-10">
|
|
119
|
+
<i class="pi pi-spin pi-spinner text-2xl text-indigo-500" />
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
<!-- Fişler yok -->
|
|
123
|
+
<div v-else-if="!orders.length" class="px-5 py-10 text-center text-paper-500 dark:text-paper-400">
|
|
124
|
+
{{ $t("no_orders_in_session") }}
|
|
125
|
+
</div>
|
|
126
|
+
|
|
127
|
+
<div v-else>
|
|
128
|
+
<!-- Özet -->
|
|
129
|
+
<div
|
|
130
|
+
class="grid grid-cols-3 gap-px bg-gray-100 dark:bg-gray-700 border-t border-paper-300 dark:border-paper-700"
|
|
131
|
+
>
|
|
132
|
+
<div class="bg-paper-50 dark:bg-paper-800 px-5 py-3 text-center">
|
|
133
|
+
<div class="pos-total-label">{{ $t("receipt_count") }}</div>
|
|
134
|
+
<div class="text-xl font-bold text-paper-900 dark:text-paper-100">{{ orders.length }}</div>
|
|
135
|
+
</div>
|
|
136
|
+
<div class="bg-paper-50 dark:bg-paper-800 px-5 py-3 text-center">
|
|
137
|
+
<div class="pos-total-label">{{ $t("total_cash") }}</div>
|
|
138
|
+
<div class="text-xl font-bold text-green-600">{{ fmtCur(selectedSession.totalCash) }}</div>
|
|
139
|
+
</div>
|
|
140
|
+
<div class="bg-paper-50 dark:bg-paper-800 px-5 py-3 text-center">
|
|
141
|
+
<div class="pos-total-label">{{ $t("total_card") }}</div>
|
|
142
|
+
<div class="text-xl font-bold text-blue-600">{{ fmtCur(selectedSession.totalCard) }}</div>
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
|
|
146
|
+
<!-- Fiş tablosu -->
|
|
147
|
+
<table class="w-full text-sm">
|
|
148
|
+
<thead class="bg-paper-100 dark:bg-paper-800/60 text-paper-600 dark:text-paper-400">
|
|
149
|
+
<tr>
|
|
150
|
+
<th class="text-left px-4 py-2">{{ $t("receipt_no") }}</th>
|
|
151
|
+
<th class="text-left px-4 py-2">{{ $t("date") }}</th>
|
|
152
|
+
<th class="text-right px-4 py-2">{{ $t("col_value") }}</th>
|
|
153
|
+
<th class="text-right px-4 py-2">KDV</th>
|
|
154
|
+
<th class="text-center px-4 py-2">{{ $t("payment_method") }}</th>
|
|
155
|
+
<th class="px-4 py-2" />
|
|
156
|
+
</tr>
|
|
157
|
+
</thead>
|
|
158
|
+
<tbody>
|
|
159
|
+
<template v-for="o in orders" :key="o.id">
|
|
160
|
+
<!-- Fiş satırı -->
|
|
161
|
+
<tr
|
|
162
|
+
class="border-t border-paper-300 dark:border-paper-800 cursor-pointer transition-colors"
|
|
163
|
+
:class="
|
|
164
|
+
expandedOrderId === o.id
|
|
165
|
+
? 'bg-indigo-50 dark:bg-indigo-950/20'
|
|
166
|
+
: 'hover:bg-gray-50 dark:hover:bg-gray-800/30'
|
|
167
|
+
"
|
|
168
|
+
@click="toggleOrder(o.id)"
|
|
169
|
+
>
|
|
170
|
+
<td class="px-4 py-2 font-semibold text-indigo-500">{{ o.orderNo }}</td>
|
|
171
|
+
<td class="px-4 py-2 text-paper-600 dark:text-paper-400 text-xs">{{ fmtDateTime(o.date) }}</td>
|
|
172
|
+
<td class="px-4 py-2 text-right tabular-nums font-semibold">{{ fmtCur(o.totalAmount) }}</td>
|
|
173
|
+
<td class="px-4 py-2 text-right tabular-nums text-paper-600 dark:text-paper-400">
|
|
174
|
+
{{ fmtCur(o.totalVat) }}
|
|
175
|
+
</td>
|
|
176
|
+
<td class="px-4 py-2 text-center">
|
|
177
|
+
<span
|
|
178
|
+
v-for="p in o.payments"
|
|
179
|
+
:key="p.id"
|
|
180
|
+
class="badge-base badge-size-xs mr-1"
|
|
181
|
+
:class="p.paymentMethod === 'CASH' ? 'badge-success' : 'badge-info'"
|
|
182
|
+
>
|
|
183
|
+
{{ p.paymentMethod === "CASH" ? $t("cash") : $t("card") }}
|
|
184
|
+
{{ fmtCur(p.amount) }}
|
|
185
|
+
</span>
|
|
186
|
+
</td>
|
|
187
|
+
<td class="px-4 py-2 text-center text-xs text-indigo-400">
|
|
188
|
+
<i :class="expandedOrderId === o.id ? 'pi pi-chevron-up' : 'pi pi-chevron-down'" />
|
|
189
|
+
</td>
|
|
190
|
+
</tr>
|
|
191
|
+
|
|
192
|
+
<!-- Kalem detayları -->
|
|
193
|
+
<tr v-if="expandedOrderId === o.id">
|
|
194
|
+
<td colspan="6" class="px-0 py-0 bg-indigo-50/50 dark:bg-indigo-950/10">
|
|
195
|
+
<table class="w-full text-xs">
|
|
196
|
+
<thead class="bg-indigo-100 dark:bg-indigo-900/30 text-indigo-700 dark:text-indigo-400">
|
|
197
|
+
<tr>
|
|
198
|
+
<th class="text-left px-8 py-1.5">{{ $t("col_code") }}</th>
|
|
199
|
+
<th class="text-left px-4 py-1.5">{{ $t("col_description") }}</th>
|
|
200
|
+
<th class="text-right px-4 py-1.5">{{ $t("col_qty") }}</th>
|
|
201
|
+
<th class="text-right px-4 py-1.5">{{ $t("col_price") }}</th>
|
|
202
|
+
<th class="text-right px-4 py-1.5">KDV%</th>
|
|
203
|
+
<th class="text-right px-4 py-1.5">{{ $t("col_value") }}</th>
|
|
204
|
+
</tr>
|
|
205
|
+
</thead>
|
|
206
|
+
<tbody>
|
|
207
|
+
<tr
|
|
208
|
+
v-for="item in o.items"
|
|
209
|
+
:key="item.id"
|
|
210
|
+
class="border-t border-indigo-100 dark:border-indigo-900/20"
|
|
211
|
+
>
|
|
212
|
+
<td class="px-8 py-1.5 font-mono text-paper-600 dark:text-paper-400">
|
|
213
|
+
{{ item.product?.code ?? "—" }}
|
|
214
|
+
</td>
|
|
215
|
+
<td class="px-4 py-1.5 text-paper-700 dark:text-paper-300">
|
|
216
|
+
{{ item.product?.name ?? "—" }}
|
|
217
|
+
</td>
|
|
218
|
+
<td class="px-4 py-1.5 text-right tabular-nums">{{ Number(item.quantity).toFixed(2) }}</td>
|
|
219
|
+
<td class="px-4 py-1.5 text-right tabular-nums">{{ fmtCur(item.unitPrice) }}</td>
|
|
220
|
+
<td class="px-4 py-1.5 text-right tabular-nums text-paper-600 dark:text-paper-400">
|
|
221
|
+
{{ item.vatRate }}%
|
|
222
|
+
</td>
|
|
223
|
+
<td class="px-4 py-1.5 text-right tabular-nums font-semibold">
|
|
224
|
+
{{ fmtCur(item.totalAmount) }}
|
|
225
|
+
</td>
|
|
226
|
+
</tr>
|
|
227
|
+
</tbody>
|
|
228
|
+
</table>
|
|
229
|
+
</td>
|
|
230
|
+
</tr>
|
|
231
|
+
</template>
|
|
232
|
+
</tbody>
|
|
233
|
+
</table>
|
|
234
|
+
</div>
|
|
235
|
+
</section>
|
|
236
|
+
</div>
|
|
237
|
+
</template>
|
|
238
|
+
</div>
|
|
239
|
+
</template>
|
|
240
|
+
|
|
241
|
+
<script setup>
|
|
242
|
+
// pages/archive.vue — Kapalı vardiyaların arşivi: fiş ve kalem detayları.
|
|
243
|
+
// resyncSession: senkron başarısız vardiyaları main'e yeniden gönderir.
|
|
244
|
+
import { formatDateTime as displayDateTime } from "@pro-acc/ui/utils/date";
|
|
245
|
+
import { ref, onMounted } from "vue";
|
|
246
|
+
import { useI18n } from "vue-i18n";
|
|
247
|
+
import { useSessionSync, syncResultMessage } from "@pro-acc/ui/pos/composables/useSessionSync.js";
|
|
248
|
+
|
|
249
|
+
const { $api } = useNuxtApp();
|
|
250
|
+
const { formatCurrency } = useCurrency();
|
|
251
|
+
const { t } = useI18n();
|
|
252
|
+
const toast = useAppToast();
|
|
253
|
+
const { sessionSyncStatus, notifySyncResult } = useSessionSync();
|
|
254
|
+
|
|
255
|
+
const loading = ref(true);
|
|
256
|
+
const closedSessions = ref([]);
|
|
257
|
+
const selectedSession = ref(null);
|
|
258
|
+
const orders = ref([]);
|
|
259
|
+
const ordersLoading = ref(false);
|
|
260
|
+
const expandedOrderId = ref(null);
|
|
261
|
+
const resyncingId = ref(null); // Yeniden gönderilen vardiyanın ID'si (spinner için)
|
|
262
|
+
|
|
263
|
+
const fmtCur = (n) => formatCurrency(n);
|
|
264
|
+
const fmtDate = (value) => displayDateTime(value, "—");
|
|
265
|
+
const fmtDateTime = (value) => displayDateTime(value, "—", true);
|
|
266
|
+
|
|
267
|
+
onMounted(async () => {
|
|
268
|
+
try {
|
|
269
|
+
const all = await $api.session.list().catch(() => []);
|
|
270
|
+
closedSessions.value = all.filter((s) => s.closedAt);
|
|
271
|
+
} finally {
|
|
272
|
+
loading.value = false;
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
async function selectSession(session) {
|
|
277
|
+
if (selectedSession.value?.id === session.id) {
|
|
278
|
+
selectedSession.value = null;
|
|
279
|
+
orders.value = [];
|
|
280
|
+
expandedOrderId.value = null;
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
selectedSession.value = session;
|
|
284
|
+
orders.value = [];
|
|
285
|
+
expandedOrderId.value = null;
|
|
286
|
+
ordersLoading.value = true;
|
|
287
|
+
try {
|
|
288
|
+
orders.value = await $api.session.fetchSessionOrders(session.id).catch(() => []);
|
|
289
|
+
} finally {
|
|
290
|
+
ordersLoading.value = false;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function toggleOrder(id) {
|
|
295
|
+
expandedOrderId.value = expandedOrderId.value === id ? null : id;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Senkronize edilemeyen bir vardiyayı main'e yeniden gönderir.
|
|
300
|
+
* Listedeki satır, sunucunun yerelde yazdığı durumla AYNI şekilde güncellenir:
|
|
301
|
+
* synced → senkronize · queued → kuyrukta (hata yok) · failed → hata.
|
|
302
|
+
*/
|
|
303
|
+
async function resyncSession(session) {
|
|
304
|
+
if (resyncingId.value) return;
|
|
305
|
+
resyncingId.value = session.id;
|
|
306
|
+
try {
|
|
307
|
+
const res = await $api.sync.pushSession(session.id);
|
|
308
|
+
const { level } = syncResultMessage(res);
|
|
309
|
+
const patch =
|
|
310
|
+
level === "success"
|
|
311
|
+
? { isSyncedToMain: true, syncError: null }
|
|
312
|
+
: level === "info"
|
|
313
|
+
? { isSyncedToMain: false, syncError: null }
|
|
314
|
+
: { isSyncedToMain: false, syncError: res?.error || "sync_failed" };
|
|
315
|
+
const idx = closedSessions.value.findIndex((s) => s.id === session.id);
|
|
316
|
+
if (idx !== -1) closedSessions.value[idx] = { ...closedSessions.value[idx], ...patch };
|
|
317
|
+
if (selectedSession.value?.id === session.id) {
|
|
318
|
+
selectedSession.value = { ...selectedSession.value, ...patch };
|
|
319
|
+
}
|
|
320
|
+
notifySyncResult(res);
|
|
321
|
+
} catch (e) {
|
|
322
|
+
toast.error(t("session_closed_not_synced"));
|
|
323
|
+
} finally {
|
|
324
|
+
resyncingId.value = null;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
</script>
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<!-- POS/Restoran ortak giriş ekranı (pos-core layer) — kullanıcılar main'de oluşturulur. -->
|
|
3
|
+
<div class="min-h-screen flex items-center justify-center bg-gray-100 dark:bg-gray-950 p-4">
|
|
4
|
+
<div class="w-full max-w-sm bg-paper-50 dark:bg-paper-900 rounded-2xl shadow-lg p-7">
|
|
5
|
+
<AuthLoginForm
|
|
6
|
+
:title="terminalName"
|
|
7
|
+
:subtitle="$t('pos_login_subtitle', 'Kasaya giriş yapın')"
|
|
8
|
+
:error="errorText"
|
|
9
|
+
:loading="loading"
|
|
10
|
+
@submit="onSubmit"
|
|
11
|
+
>
|
|
12
|
+
<template #icon>
|
|
13
|
+
<i class="pi pi-bolt text-yellow-400 text-4xl" />
|
|
14
|
+
</template>
|
|
15
|
+
</AuthLoginForm>
|
|
16
|
+
</div>
|
|
17
|
+
</div>
|
|
18
|
+
</template>
|
|
19
|
+
|
|
20
|
+
<script setup>
|
|
21
|
+
// login.vue — POS/Restoran ortak giriş sayfası (layout'suz). AuthLoginForm (pos-ui) + auth store.
|
|
22
|
+
import { ref, computed, onMounted } from "vue";
|
|
23
|
+
import { useI18n } from "vue-i18n";
|
|
24
|
+
import { useAuthStore } from "@pro-acc/ui/pos/stores/auth.store.js";
|
|
25
|
+
import { useSettingsStore } from "@pro-acc/ui/pos/stores/settings.store.js";
|
|
26
|
+
import { useTerminalName } from "@pro-acc/ui/pos/composables/useTerminalName.js";
|
|
27
|
+
|
|
28
|
+
definePageMeta({ layout: false });
|
|
29
|
+
|
|
30
|
+
const { t } = useI18n();
|
|
31
|
+
const auth = useAuthStore();
|
|
32
|
+
const route = useRoute();
|
|
33
|
+
const settingsStore = useSettingsStore();
|
|
34
|
+
const terminalName = useTerminalName();
|
|
35
|
+
|
|
36
|
+
const loading = ref(false);
|
|
37
|
+
const error = ref("");
|
|
38
|
+
const errorText = computed(() => (error.value ? t(error.value, error.value) : ""));
|
|
39
|
+
|
|
40
|
+
onMounted(() => {
|
|
41
|
+
if (!settingsStore.settings) settingsStore.fetch().catch(() => {});
|
|
42
|
+
// Kasada klavye odaklı akış: e-posta alanına otomatik odaklan
|
|
43
|
+
document.getElementById("email")?.focus();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
async function onSubmit({ email, password }) {
|
|
47
|
+
loading.value = true;
|
|
48
|
+
error.value = "";
|
|
49
|
+
try {
|
|
50
|
+
await auth.login({ email: email.trim(), password });
|
|
51
|
+
await navigateTo(route.query.redirect || "/");
|
|
52
|
+
} catch (e) {
|
|
53
|
+
error.value = e?.data?.statusMessage || "v_invalid_credentials";
|
|
54
|
+
} finally {
|
|
55
|
+
loading.value = false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
</script>
|