@pro-acc/pos-core 0.0.0-stage → 0.0.2

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.
Files changed (89) hide show
  1. package/README.md +16 -2
  2. package/app/app.vue +29 -0
  3. package/app/layouts/pos.vue +14 -0
  4. package/app/middleware/auth.global.ts +43 -0
  5. package/app/pages/archive.vue +327 -0
  6. package/app/pages/login.vue +58 -0
  7. package/app/pages/sessions/index.vue +198 -0
  8. package/app/pages/settings.vue +381 -0
  9. package/app/repository/factory.ts +121 -0
  10. package/app/repository/modules/auth.ts +26 -0
  11. package/app/repository/modules/bankTerminal.ts +11 -0
  12. package/app/repository/modules/order.ts +15 -0
  13. package/app/repository/modules/product.ts +13 -0
  14. package/app/repository/modules/session.ts +37 -0
  15. package/app/repository/modules/settings.ts +14 -0
  16. package/app/repository/modules/sync.ts +36 -0
  17. package/env.d.ts +5 -0
  18. package/i18n/locales/en.json +289 -0
  19. package/i18n/locales/sq.json +291 -0
  20. package/i18n/locales/tr.json +291 -0
  21. package/nuxt.config.ts +98 -0
  22. package/package.json +205 -4
  23. package/security/app/error.vue +4 -0
  24. package/security/nuxt.config.ts +15 -0
  25. package/security/server/api/system/update-check.get.ts +9 -0
  26. package/security/server/middleware/10-security-headers.ts +45 -0
  27. package/security/server/middleware/11-csrf-origin.ts +45 -0
  28. package/security/server/utils/rateLimit.ts +89 -0
  29. package/security/server/utils/update-check.ts +93 -0
  30. package/server/api/auth/change-password.post.ts +27 -0
  31. package/server/api/auth/login.post.ts +53 -0
  32. package/server/api/auth/logout.post.ts +7 -0
  33. package/server/api/auth/me.get.ts +15 -0
  34. package/server/api/bank-terminals/[id].delete.ts +8 -0
  35. package/server/api/bank-terminals/[id].put.ts +10 -0
  36. package/server/api/bank-terminals/index.get.ts +6 -0
  37. package/server/api/bank-terminals/index.post.ts +9 -0
  38. package/server/api/customers/index.get.ts +7 -0
  39. package/server/api/orders/index.get.ts +6 -0
  40. package/server/api/orders/index.post.ts +10 -0
  41. package/server/api/products/[id].get.ts +10 -0
  42. package/server/api/products/by-code/[code].get.ts +10 -0
  43. package/server/api/products/index.get.ts +6 -0
  44. package/server/api/seed.post.ts +40 -0
  45. package/server/api/session/close.post.ts +13 -0
  46. package/server/api/session/current.get.ts +6 -0
  47. package/server/api/session/open.post.ts +6 -0
  48. package/server/api/sessions/[id]/orders.get.ts +16 -0
  49. package/server/api/sessions/[id].get.ts +10 -0
  50. package/server/api/sessions/index.get.ts +6 -0
  51. package/server/api/settings/index.get.ts +6 -0
  52. package/server/api/settings/index.put.ts +9 -0
  53. package/server/api/sync/accounts.get.ts +6 -0
  54. package/server/api/sync/all.post.ts +22 -0
  55. package/server/api/sync/bootstrap.post.ts +6 -0
  56. package/server/api/sync/companies.get.ts +6 -0
  57. package/server/api/sync/gl-accounts.get.ts +6 -0
  58. package/server/api/sync/health.get.ts +6 -0
  59. package/server/api/sync/products.post.ts +6 -0
  60. package/server/api/sync/session.post.ts +8 -0
  61. package/server/api/sync/test.post.ts +19 -0
  62. package/server/api/sync/users.post.ts +4 -0
  63. package/server/api/sync/warehouses.get.ts +6 -0
  64. package/server/middleware/00-sw-noop.ts +12 -0
  65. package/server/middleware/01-pos-auth.ts +37 -0
  66. package/server/plugins/00-env-check.ts +14 -0
  67. package/server/repository/base.repository.ts +52 -0
  68. package/server/service/auth.base.ts +74 -0
  69. package/server/service/bank-terminal.base.ts +33 -0
  70. package/server/service/customer.base.ts +51 -0
  71. package/server/service/order.base.ts +185 -0
  72. package/server/service/product.base.ts +43 -0
  73. package/server/service/session.base.ts +169 -0
  74. package/server/service/settings.base.ts +83 -0
  75. package/server/service/sync.base.ts +368 -0
  76. package/server/shared/fiscalPrinter.ts +221 -0
  77. package/server/utils/apiHandler.ts +32 -0
  78. package/server/utils/crudHandlers.ts +97 -0
  79. package/server/utils/customer.ts +42 -0
  80. package/server/utils/decimal.ts +9 -0
  81. package/server/utils/orderMath.ts +21 -0
  82. package/server/utils/posAuth.ts +64 -0
  83. package/server/utils/sessionPayload.ts +87 -0
  84. package/server/utils/tender.ts +78 -0
  85. package/server/utils/terminalSession.ts +63 -0
  86. package/server/validation/bank-terminal.validation.ts +15 -0
  87. package/server/validation/order.validation.ts +76 -0
  88. package/server/validation/product.validation.ts +29 -0
  89. package/server/validation/settings.validation.ts +23 -0
package/README.md CHANGED
@@ -1,3 +1,17 @@
1
- # Temporary Holding Version
1
+ # @pro-acc/pos-core
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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.2
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
+ CRUD factory 0.0.2 ile aynı Nuxt fetcher'ındaki eşzamanlı, eşit GET sorgularını birleştirir. Tamamlanan yanıtlar önbellekte tutulmaz; mutasyon eski uçuşları geçersizleştirir. Özel headers/signal/hook taşıyan istekler paylaşılmaz. Farklı fetcher/Nuxt örnekleri birbirinden izoledir. Main aynı factory'yi `@pro-acc/pos-core/app/repository/factory` export'undan kullanabilir.
16
+
17
+ [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>