@cavulsqa/create 2.9.3 → 2.10.0
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 +12 -1
- package/bin/create.mjs +18 -25
- package/lib/templates.mjs +56 -0
- package/package.json +13 -5
- package/templates/f7-app/auto-imports.d.ts +9 -0
- package/templates/f7-app/package.json +6 -3
- package/templates/f7-app/src/env.d.ts +1 -0
- package/templates/f7-app/src/main.ts +3 -0
- package/templates/f7-app/src/plugins/bootstrapError.ts +2 -0
- package/templates/f7-app/src/plugins/recorder.plugin.ts +155 -0
- package/templates/f7-app/src/shared/recorder/capacitorSink.ts +124 -0
- package/templates/f7-app/vite.config.ts +1 -0
- package/templates/m3e-app/.claude/rules/data-fetching.md +68 -0
- package/templates/m3e-app/.claude/rules/database.md +106 -0
- package/templates/m3e-app/.claude/rules/m3e-ui.md +48 -0
- package/templates/m3e-app/.claude/rules/modules.md +43 -0
- package/templates/m3e-app/.claude/rules/native.md +60 -0
- package/templates/m3e-app/.claude/skills/m3-expressive/SKILL.md +68 -0
- package/templates/m3e-app/.claude/skills/m3-expressive/color.md +44 -0
- package/templates/m3e-app/.claude/skills/m3-expressive/components.md +325 -0
- package/templates/m3e-app/.claude/skills/m3-expressive/layout.md +42 -0
- package/templates/m3e-app/.claude/skills/m3-expressive/motion.md +51 -0
- package/templates/m3e-app/.claude/skills/m3-expressive/shapes-type.md +48 -0
- package/templates/m3e-app/.claude/skills/m3-expressive/sources.md +53 -0
- package/templates/m3e-app/.claude/skills/module-architecture/SKILL.md +65 -0
- package/templates/m3e-app/.claude/skills/module-architecture/file-templates.md +178 -0
- package/templates/m3e-app/.claude/skills/reactive-data/SKILL.md +90 -0
- package/templates/m3e-app/.claude/skills/reactive-data/testing.md +55 -0
- package/templates/m3e-app/.env.example +32 -0
- package/templates/m3e-app/CLAUDE.md +93 -0
- package/templates/m3e-app/auto-imports.d.ts +851 -0
- package/templates/m3e-app/capacitor.config.ts +44 -0
- package/templates/m3e-app/components.d.ts +252 -0
- package/templates/m3e-app/index.html +16 -0
- package/templates/m3e-app/package.json +107 -0
- package/templates/m3e-app/src/App.vue +129 -0
- package/templates/m3e-app/src/app/pragmas.config.ts +35 -0
- package/templates/m3e-app/src/app/scroll.config.ts +20 -0
- package/templates/m3e-app/src/app/storage.config.ts +65 -0
- package/templates/m3e-app/src/app/tabs.ts +33 -0
- package/templates/m3e-app/src/app/theme.config.ts +51 -0
- package/templates/m3e-app/src/assets/css/app.css +18 -0
- package/templates/m3e-app/src/assets/css/base.css +53 -0
- package/templates/m3e-app/src/assets/css/layout/container-transform.css +113 -0
- package/templates/m3e-app/src/assets/css/layout/shell.css +69 -0
- package/templates/m3e-app/src/assets/css/layout/transitions.css +121 -0
- package/templates/m3e-app/src/assets/css/m3e.css +6 -0
- package/templates/m3e-app/src/assets/css/theme/framework7.css +18 -0
- package/templates/m3e-app/src/assets/css/theme/tailwind.css +365 -0
- package/templates/m3e-app/src/domains/benchmark/benchmark.dataset.ts +209 -0
- package/templates/m3e-app/src/domains/benchmark/benchmark.suite.ts +662 -0
- package/templates/m3e-app/src/domains/sales/sales.repository.ts +458 -0
- package/templates/m3e-app/src/env.d.ts +40 -0
- package/templates/m3e-app/src/locales/ar.json +1260 -0
- package/templates/m3e-app/src/locales/en.json +1260 -0
- package/templates/m3e-app/src/locales/fr.json +1260 -0
- package/templates/m3e-app/src/main.ts +41 -0
- package/templates/m3e-app/src/modules/demo/components/DemoBenchmark.vue +119 -0
- package/templates/m3e-app/src/modules/demo/components/DemoBusLog.vue +63 -0
- package/templates/m3e-app/src/modules/demo/components/DemoCreateOrderSheet.vue +203 -0
- package/templates/m3e-app/src/modules/demo/components/DemoMetricsSheet.vue +75 -0
- package/templates/m3e-app/src/modules/demo/components/DemoOrderList.vue +75 -0
- package/templates/m3e-app/src/modules/demo/components/DemoPipelineBenchmark.vue +65 -0
- package/templates/m3e-app/src/modules/demo/components/DemoStatCards.vue +71 -0
- package/templates/m3e-app/src/modules/demo/composables/useBenchmark.ts +139 -0
- package/templates/m3e-app/src/modules/demo/composables/useOrderStatus.ts +64 -0
- package/templates/m3e-app/src/modules/demo/composables/useReactiveDemo.ts +171 -0
- package/templates/m3e-app/src/modules/demo/router/routes/demo.routes.ts +22 -0
- package/templates/m3e-app/src/modules/demo/views/DemoView.vue +210 -0
- package/templates/m3e-app/src/modules/demo/views/OrderDetailView.vue +136 -0
- package/templates/m3e-app/src/modules/demo/views/OrderSearchView.vue +74 -0
- package/templates/m3e-app/src/modules/gallery/components/GalleryBlock.vue +16 -0
- package/templates/m3e-app/src/modules/gallery/components/GalleryCarouselTile.vue +71 -0
- package/templates/m3e-app/src/modules/gallery/components/GalleryCustomerForm.vue +76 -0
- package/templates/m3e-app/src/modules/gallery/components/GalleryProofOfDelivery.vue +69 -0
- package/templates/m3e-app/src/modules/gallery/components/agenda/AgendaDay.vue +106 -0
- package/templates/m3e-app/src/modules/gallery/components/agenda/AgendaMonth.vue +40 -0
- package/templates/m3e-app/src/modules/gallery/components/agenda/AgendaVisitList.vue +45 -0
- package/templates/m3e-app/src/modules/gallery/components/chat/ChatAttachSheet.vue +106 -0
- package/templates/m3e-app/src/modules/gallery/components/chat/ChatContactPicker.vue +85 -0
- package/templates/m3e-app/src/modules/gallery/components/chat/ChatInviteComposer.vue +131 -0
- package/templates/m3e-app/src/modules/gallery/components/chat/ChatPollComposer.vue +148 -0
- package/templates/m3e-app/src/modules/gallery/components/chat/ChatRecentPhotos.vue +62 -0
- package/templates/m3e-app/src/modules/gallery/components/inputs/InputsAccount.vue +79 -0
- package/templates/m3e-app/src/modules/gallery/components/inputs/InputsTags.vue +53 -0
- package/templates/m3e-app/src/modules/gallery/components/inputs/InputsVerification.vue +73 -0
- package/templates/m3e-app/src/modules/gallery/components/inputs/PasswordStrength.vue +37 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GalleryButtons.vue +128 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GalleryCarousels.vue +236 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GalleryCharts.vue +84 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GalleryFabs.vue +70 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GalleryInputs.vue +219 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GalleryNavigation.vue +141 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GalleryOverlays.vue +368 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GalleryPickers.vue +175 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GalleryProgress.vue +118 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GalleryScale.vue +103 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GallerySelection.vue +219 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GalleryShapes.vue +48 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GallerySurfaces.vue +213 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GalleryTables.vue +185 -0
- package/templates/m3e-app/src/modules/gallery/components/surfaces/SurfacesContainerTransform.vue +46 -0
- package/templates/m3e-app/src/modules/gallery/composables/chatTeam.ts +18 -0
- package/templates/m3e-app/src/modules/gallery/composables/composeKind.ts +2 -0
- package/templates/m3e-app/src/modules/gallery/composables/currentPosition.ts +35 -0
- package/templates/m3e-app/src/modules/gallery/composables/demoOrders.ts +29 -0
- package/templates/m3e-app/src/modules/gallery/composables/featuredStories.ts +73 -0
- package/templates/m3e-app/src/modules/gallery/composables/fieldFormats.ts +36 -0
- package/templates/m3e-app/src/modules/gallery/composables/openExternal.ts +11 -0
- package/templates/m3e-app/src/modules/gallery/composables/routePlan.ts +90 -0
- package/templates/m3e-app/src/modules/gallery/composables/useAgenda.ts +61 -0
- package/templates/m3e-app/src/modules/gallery/composables/useChatCustomers.ts +31 -0
- package/templates/m3e-app/src/modules/gallery/composables/useChatDemo.ts +299 -0
- package/templates/m3e-app/src/modules/gallery/composables/useGallerySections.ts +230 -0
- package/templates/m3e-app/src/modules/gallery/composables/useLocalAttachments.ts +71 -0
- package/templates/m3e-app/src/modules/gallery/composables/useLocationShare.ts +38 -0
- package/templates/m3e-app/src/modules/gallery/composables/usePhotoScenes.ts +82 -0
- package/templates/m3e-app/src/modules/gallery/composables/useTreeDemo.ts +67 -0
- package/templates/m3e-app/src/modules/gallery/composables/wilayas.ts +67 -0
- package/templates/m3e-app/src/modules/gallery/router/routes/gallery.routes.ts +52 -0
- package/templates/m3e-app/src/modules/gallery/views/GalleryAgendaView.vue +117 -0
- package/templates/m3e-app/src/modules/gallery/views/GalleryChatView.vue +292 -0
- package/templates/m3e-app/src/modules/gallery/views/GalleryContactsView.vue +77 -0
- package/templates/m3e-app/src/modules/gallery/views/GalleryFeaturedView.vue +60 -0
- package/templates/m3e-app/src/modules/gallery/views/GalleryLoginView.vue +135 -0
- package/templates/m3e-app/src/modules/gallery/views/GalleryOnboardingView.vue +116 -0
- package/templates/m3e-app/src/modules/gallery/views/GallerySectionView.vue +27 -0
- package/templates/m3e-app/src/modules/gallery/views/GalleryTabsView.vue +135 -0
- package/templates/m3e-app/src/modules/gallery/views/GalleryView.vue +50 -0
- package/templates/m3e-app/src/modules/home/components/HomeHero.vue +57 -0
- package/templates/m3e-app/src/modules/home/composables/useHomeFeatures.ts +128 -0
- package/templates/m3e-app/src/modules/home/composables/useOpenFeature.ts +22 -0
- package/templates/m3e-app/src/modules/home/router/routes/home.routes.ts +21 -0
- package/templates/m3e-app/src/modules/home/views/FeatureDetailView.vue +66 -0
- package/templates/m3e-app/src/modules/home/views/HomeView.vue +47 -0
- package/templates/m3e-app/src/modules/settings/components/RolePalette.vue +38 -0
- package/templates/m3e-app/src/modules/settings/components/SeedSwatches.vue +41 -0
- package/templates/m3e-app/src/modules/settings/components/SettingsChoice.vue +43 -0
- package/templates/m3e-app/src/modules/settings/components/StudioPreview.vue +52 -0
- package/templates/m3e-app/src/modules/settings/router/routes/settings.routes.ts +17 -0
- package/templates/m3e-app/src/modules/settings/types.ts +7 -0
- package/templates/m3e-app/src/modules/settings/views/ColorStudioView.vue +166 -0
- package/templates/m3e-app/src/modules/settings/views/SettingsView.vue +157 -0
- package/templates/m3e-app/src/plugins/bootstrapError.ts +62 -0
- package/templates/m3e-app/src/plugins/capacitor/index.ts +14 -0
- package/templates/m3e-app/src/plugins/capacitor/useAndroidBackButton.ts +36 -0
- package/templates/m3e-app/src/plugins/capacitor/useKeyboard.ts +48 -0
- package/templates/m3e-app/src/plugins/capacitor/useSplashScreen.ts +43 -0
- package/templates/m3e-app/src/plugins/capacitor/useStatusBar.ts +16 -0
- package/templates/m3e-app/src/plugins/framework7.plugin.ts +36 -0
- package/templates/m3e-app/src/plugins/i18n.plugin.ts +87 -0
- package/templates/m3e-app/src/plugins/m3e.plugin.ts +22 -0
- package/templates/m3e-app/src/plugins/seed.plugin.ts +7 -0
- package/templates/m3e-app/src/plugins/sqlite.plugin.ts +32 -0
- package/templates/m3e-app/src/router/global/global.routes.ts +12 -0
- package/templates/m3e-app/src/router/index.ts +20 -0
- package/templates/m3e-app/src/shared/components/error/404.vue +13 -0
- package/templates/m3e-app/src/shared/components/layout/EmptyState.vue +30 -0
- package/templates/m3e-app/src/shared/components/layout/SectionHeader.vue +10 -0
- package/templates/m3e-app/src/shared/components/page/AppPage.vue +104 -0
- package/templates/m3e-app/src/shared/composables/navigation/useActiveTab.ts +39 -0
- package/templates/m3e-app/src/shared/composables/navigation/useContainerTransform.ts +117 -0
- package/templates/m3e-app/src/shared/composables/navigation/useNavigationGuard.ts +48 -0
- package/templates/m3e-app/src/shared/composables/navigation/useNavigationVisibility.ts +64 -0
- package/templates/m3e-app/src/shared/composables/navigation/useViewRouter.ts +25 -0
- package/templates/m3e-app/src/shared/composables/navigation/useWindowClass.ts +22 -0
- package/templates/m3e-app/src/shared/composables/theme/useThemeSettings.ts +155 -0
- package/templates/m3e-app/src/shared/database/candidates/capacitorSqlite.ts +36 -0
- package/templates/m3e-app/src/shared/database/candidates/index.ts +4 -0
- package/templates/m3e-app/src/shared/database/candidates/opfsSahPool.ts +25 -0
- package/templates/m3e-app/src/shared/database/candidates/types.ts +56 -0
- package/templates/m3e-app/src/shared/database/candidates/waSqlite.ts +56 -0
- package/templates/m3e-app/src/shared/database/database.ts +219 -0
- package/templates/m3e-app/src/shared/database/index.ts +3 -0
- package/templates/m3e-app/src/shared/database/migrations.ts +223 -0
- package/templates/m3e-app/src/shared/database/opfs.worker.ts +5 -0
- package/templates/m3e-app/src/shared/database/queries.ts +26 -0
- package/templates/m3e-app/src/shared/database/schema.ts +153 -0
- package/templates/m3e-app/src/shared/database/storage.ts +64 -0
- package/templates/m3e-app/src/shared/database/wa.worker.ts +5 -0
- package/templates/m3e-app/src/shared/utils/lazyRoute.ts +41 -0
- package/templates/m3e-app/src/shared/utils/resolvers/resolvers.ts +42 -0
- package/templates/m3e-app/src/shared/utils/textDirection.ts +22 -0
- package/templates/m3e-app/src/shared/utils/theme/themeSettings.ts +59 -0
- package/templates/m3e-app/src/shared/utils/tone.ts +8 -0
- package/templates/m3e-app/tests/benchmark.suite.test.ts +104 -0
- package/templates/m3e-app/tests/containerTransform.test.ts +58 -0
- package/templates/m3e-app/tests/fieldFormats.test.ts +25 -0
- package/templates/m3e-app/tests/lazyRoute.test.ts +30 -0
- package/templates/m3e-app/tests/locales.test.ts +73 -0
- package/templates/m3e-app/tests/migrations.test.ts +74 -0
- package/templates/m3e-app/tests/navigationGuard.test.ts +82 -0
- package/templates/m3e-app/tests/openDatabase.test.ts +54 -0
- package/templates/m3e-app/tests/sales.repository.test.ts +329 -0
- package/templates/m3e-app/tests/storage.test.ts +117 -0
- package/templates/m3e-app/tests/textDirection.test.ts +11 -0
- package/templates/m3e-app/tooling/linkedPackages.ts +112 -0
- package/templates/m3e-app/tsconfig.json +21 -0
- package/templates/m3e-app/vite.config.ts +144 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<GalleryBlock :title="t('gallery.tables.orders')" :note="t('gallery.tables.ordersNote')" stack>
|
|
3
|
+
<div class="flex flex-wrap items-center gap-2">
|
|
4
|
+
<M3Chip
|
|
5
|
+
kind="filter"
|
|
6
|
+
:label="t('gallery.tables.dense')"
|
|
7
|
+
:selected="dense"
|
|
8
|
+
@update:selected="dense = $event"
|
|
9
|
+
/>
|
|
10
|
+
<M3Chip
|
|
11
|
+
v-for="count in ORDER_COUNTS"
|
|
12
|
+
:key="count"
|
|
13
|
+
kind="filter"
|
|
14
|
+
:label="t('gallery.tables.rowCount', { count: count.toLocaleString(locale) })"
|
|
15
|
+
:selected="size === count"
|
|
16
|
+
@update:selected="size = count"
|
|
17
|
+
/>
|
|
18
|
+
<span class="type-body-medium ms-auto text-on-surface-variant">{{
|
|
19
|
+
t("gallery.tables.selected", { count: selected.length }, selected.length)
|
|
20
|
+
}}</span>
|
|
21
|
+
</div>
|
|
22
|
+
<M3DataTable
|
|
23
|
+
v-model:sort="sort"
|
|
24
|
+
v-model:selected="selected"
|
|
25
|
+
v-model:group="layout.group"
|
|
26
|
+
v-model:pinned="layout.pinned"
|
|
27
|
+
v-model:hidden="layout.hidden"
|
|
28
|
+
class="-mx-2"
|
|
29
|
+
:rows="orders"
|
|
30
|
+
:columns="columns"
|
|
31
|
+
:row-key="orderKey"
|
|
32
|
+
:label="t('gallery.tables.orders')"
|
|
33
|
+
:locale="locale"
|
|
34
|
+
selectable
|
|
35
|
+
sticky-first-column
|
|
36
|
+
max-height="420px"
|
|
37
|
+
:dense="dense"
|
|
38
|
+
:select-all-label="t('gallery.tables.selectAll')"
|
|
39
|
+
:select-row-label="selectRowLabel"
|
|
40
|
+
:select-group-label="selectGroupLabel"
|
|
41
|
+
:group-count-label="groupCount"
|
|
42
|
+
:menu-labels="menuLabels"
|
|
43
|
+
@row-click="openOrder"
|
|
44
|
+
>
|
|
45
|
+
<template #cell="{ row, column, text }">
|
|
46
|
+
<span
|
|
47
|
+
v-if="column.key === 'status'"
|
|
48
|
+
:class="['gallery-status', `gallery-status--${row.status}`]"
|
|
49
|
+
>{{ text }}</span
|
|
50
|
+
>
|
|
51
|
+
<template v-else>{{ text }}</template>
|
|
52
|
+
</template>
|
|
53
|
+
<template #footer>
|
|
54
|
+
<tr>
|
|
55
|
+
<td :colspan="span" class="text-end tabular-nums">
|
|
56
|
+
{{ t("gallery.tables.total") }} · {{ money(total) }}
|
|
57
|
+
</td>
|
|
58
|
+
</tr>
|
|
59
|
+
</template>
|
|
60
|
+
</M3DataTable>
|
|
61
|
+
</GalleryBlock>
|
|
62
|
+
</template>
|
|
63
|
+
|
|
64
|
+
<script setup lang="ts">
|
|
65
|
+
import type { DataColumn, DataSort } from "@cavulsqa/m3e-vue";
|
|
66
|
+
import GalleryBlock from "@/modules/gallery/components/GalleryBlock.vue";
|
|
67
|
+
import { ORDER_COUNTS, makeOrders, type Order } from "@/modules/gallery/composables/demoOrders";
|
|
68
|
+
|
|
69
|
+
const { t, locale } = useI18n();
|
|
70
|
+
const snackbar = useSnackbar();
|
|
71
|
+
const sort = ref<DataSort | null>({ key: "date", direction: "descending" });
|
|
72
|
+
const selected = ref<(string | number)[]>([]);
|
|
73
|
+
const dense = ref(false);
|
|
74
|
+
const size = ref<(typeof ORDER_COUNTS)[number]>(ORDER_COUNTS[0]);
|
|
75
|
+
const orders = shallowRef<Order[]>(makeOrders(size.value));
|
|
76
|
+
watch(size, (count) => {
|
|
77
|
+
selected.value = [];
|
|
78
|
+
orders.value = makeOrders(count);
|
|
79
|
+
});
|
|
80
|
+
const layout = useLocalStorage<{ group: string | null; pinned: string[]; hidden: string[] }>(
|
|
81
|
+
"gallery.orders.layout",
|
|
82
|
+
{ group: null, pinned: [], hidden: [] },
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
const money = (cents: number) =>
|
|
86
|
+
new Intl.NumberFormat(locale.value, { style: "currency", currency: "DZD" }).format(cents / 100);
|
|
87
|
+
|
|
88
|
+
const columns = computed<DataColumn<Order>[]>(() => [
|
|
89
|
+
{ key: "ref", label: t("gallery.tables.ref"), sortable: true },
|
|
90
|
+
{ key: "customer", label: t("gallery.tables.customer"), sortable: true },
|
|
91
|
+
{ key: "wilaya", label: t("gallery.inputs.wilaya"), sortable: true },
|
|
92
|
+
{
|
|
93
|
+
key: "date",
|
|
94
|
+
label: t("gallery.tables.date"),
|
|
95
|
+
sortable: true,
|
|
96
|
+
format: (value) =>
|
|
97
|
+
typeof value === "string" ? new Date(value).toLocaleDateString(locale.value) : "",
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
key: "status",
|
|
101
|
+
label: t("gallery.tables.status"),
|
|
102
|
+
sortable: true,
|
|
103
|
+
format: (value) => t(`gallery.inputs.statuses.${String(value)}`),
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
key: "totalCents",
|
|
107
|
+
label: t("gallery.tables.totalColumn"),
|
|
108
|
+
numeric: true,
|
|
109
|
+
sortable: true,
|
|
110
|
+
summary: "sum",
|
|
111
|
+
format: (value) => (typeof value === "number" ? money(value) : ""),
|
|
112
|
+
formatSummary: money,
|
|
113
|
+
},
|
|
114
|
+
]);
|
|
115
|
+
|
|
116
|
+
const span = computed(() => columns.value.length - layout.value.hidden.length + 1);
|
|
117
|
+
|
|
118
|
+
const menuLabels = computed(() => ({
|
|
119
|
+
sortAscending: t("gallery.tables.menu.sortAscending"),
|
|
120
|
+
sortDescending: t("gallery.tables.menu.sortDescending"),
|
|
121
|
+
clearSort: t("gallery.tables.menu.clearSort"),
|
|
122
|
+
groupBy: t("gallery.tables.menu.groupBy"),
|
|
123
|
+
ungroup: t("gallery.tables.menu.ungroup"),
|
|
124
|
+
pin: t("gallery.tables.menu.pin"),
|
|
125
|
+
unpin: t("gallery.tables.menu.unpin"),
|
|
126
|
+
hide: t("gallery.tables.menu.hide"),
|
|
127
|
+
columns: t("gallery.tables.menu.columns"),
|
|
128
|
+
}));
|
|
129
|
+
|
|
130
|
+
function groupCount(count: number) {
|
|
131
|
+
return t("gallery.tables.groupCount", { count }, count);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function selectGroupLabel(label: string) {
|
|
135
|
+
return t("gallery.tables.selectGroup", { label });
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const total = computed(() => {
|
|
139
|
+
const chosen = new Set(selected.value);
|
|
140
|
+
let sum = 0;
|
|
141
|
+
for (const order of orders.value) {
|
|
142
|
+
if (chosen.size === 0 || chosen.has(order.ref)) sum += order.totalCents;
|
|
143
|
+
}
|
|
144
|
+
return sum;
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
function openOrder(order: Order) {
|
|
148
|
+
void snackbar.show(t("gallery.tables.opened", { ref: order.ref }));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function orderKey(order: Order) {
|
|
152
|
+
return order.ref;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function selectRowLabel(order: Order) {
|
|
156
|
+
return t("gallery.tables.selectRow", { ref: order.ref });
|
|
157
|
+
}
|
|
158
|
+
</script>
|
|
159
|
+
|
|
160
|
+
<style scoped>
|
|
161
|
+
.gallery-status {
|
|
162
|
+
display: inline-flex;
|
|
163
|
+
align-items: center;
|
|
164
|
+
height: 24px;
|
|
165
|
+
padding: 0 10px;
|
|
166
|
+
border-radius: 12px;
|
|
167
|
+
font: var(--md-sys-typescale-label-medium-weight) var(--md-sys-typescale-label-medium-size) /
|
|
168
|
+
var(--md-sys-typescale-label-medium-line-height) var(--md-sys-typescale-label-medium-font);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.gallery-status--draft {
|
|
172
|
+
background: var(--md-sys-color-surface-container-highest);
|
|
173
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.gallery-status--confirmed {
|
|
177
|
+
background: var(--md-sys-color-secondary-container);
|
|
178
|
+
color: var(--md-sys-color-on-secondary-container);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.gallery-status--delivered {
|
|
182
|
+
background: var(--md-sys-color-tertiary-container);
|
|
183
|
+
color: var(--md-sys-color-on-tertiary-container);
|
|
184
|
+
}
|
|
185
|
+
</style>
|
package/templates/m3e-app/src/modules/gallery/components/surfaces/SurfacesContainerTransform.vue
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<GalleryBlock
|
|
3
|
+
:title="t('gallery.surfaces.containerTransform')"
|
|
4
|
+
:note="t('gallery.surfaces.containerTransformNote')"
|
|
5
|
+
stack
|
|
6
|
+
>
|
|
7
|
+
<div ref="grid" class="grid grid-cols-2 gap-3">
|
|
8
|
+
<M3Card
|
|
9
|
+
v-for="(story, index) in featuredStories"
|
|
10
|
+
:key="story.id"
|
|
11
|
+
variant="filled"
|
|
12
|
+
clickable
|
|
13
|
+
:class="[TONE_CLASSES[story.tone], index === 0 && 'col-span-2']"
|
|
14
|
+
@click="openStory(story, $event)"
|
|
15
|
+
>
|
|
16
|
+
<div class="flex h-full flex-col gap-3 p-4" :class="index === 0 && 'min-h-36'">
|
|
17
|
+
<M3Shape :shape="story.shape" class="size-12 bg-surface/60">
|
|
18
|
+
<component :is="story.icon" class="size-6" />
|
|
19
|
+
</M3Shape>
|
|
20
|
+
<span class="mt-auto flex flex-col gap-1">
|
|
21
|
+
<span class="type-title-medium">{{ t(`gallery.featured.${story.id}.title`) }}</span>
|
|
22
|
+
<span class="type-body-medium opacity-80">{{
|
|
23
|
+
t(`gallery.featured.${story.id}.text`)
|
|
24
|
+
}}</span>
|
|
25
|
+
</span>
|
|
26
|
+
</div>
|
|
27
|
+
</M3Card>
|
|
28
|
+
</div>
|
|
29
|
+
</GalleryBlock>
|
|
30
|
+
</template>
|
|
31
|
+
|
|
32
|
+
<script setup lang="ts">
|
|
33
|
+
import GalleryBlock from "@/modules/gallery/components/GalleryBlock.vue";
|
|
34
|
+
import { featuredStories, type FeaturedStory } from "@/modules/gallery/composables/featuredStories";
|
|
35
|
+
import { TONE_CLASSES } from "@/shared/utils/tone";
|
|
36
|
+
|
|
37
|
+
const { t } = useI18n();
|
|
38
|
+
const grid = useTemplateRef<HTMLElement>("grid");
|
|
39
|
+
const { router } = useViewRouter(grid);
|
|
40
|
+
const { open } = useContainerTransform();
|
|
41
|
+
|
|
42
|
+
function openStory(story: FeaturedStory, event: MouseEvent) {
|
|
43
|
+
const current = router();
|
|
44
|
+
if (current) open(current, event, `/gallery/featured/${story.id}/`);
|
|
45
|
+
}
|
|
46
|
+
</script>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { MessageInvite, MessagePoll, RsvpAnswer } from "@cavulsqa/m3e-vue";
|
|
2
|
+
|
|
3
|
+
/** A teammate's vote on option `index`: counted, but not the owner's. */
|
|
4
|
+
export function teammateVote(poll: MessagePoll, index: number): MessagePoll {
|
|
5
|
+
const target = poll.options[index % Math.max(1, poll.options.length)];
|
|
6
|
+
if (!target) return poll;
|
|
7
|
+
return {
|
|
8
|
+
...poll,
|
|
9
|
+
options: poll.options.map((option) =>
|
|
10
|
+
option.id === target.id ? { ...option, votes: option.votes + 1 } : option,
|
|
11
|
+
),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** A teammate's answer to an invite: counted, but not the owner's. */
|
|
16
|
+
export function teammateAnswer(invite: MessageInvite, answer: RsvpAnswer): MessageInvite {
|
|
17
|
+
return { ...invite, answers: { ...invite.answers, [answer]: invite.answers[answer] + 1 } };
|
|
18
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export type PositionFailure = "unsupported" | "denied" | "unavailable" | "timeout";
|
|
2
|
+
|
|
3
|
+
export class PositionError extends Error {
|
|
4
|
+
constructor(readonly reason: PositionFailure) {
|
|
5
|
+
super(`location ${reason}`);
|
|
6
|
+
this.name = "PositionError";
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const REASONS: Record<number, PositionFailure> = { 1: "denied", 2: "unavailable", 3: "timeout" };
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Where the device is, through the WebView's own geolocation - no plugin. Coarse first: a fix from
|
|
14
|
+
* the network comes back in a second where GPS can take a minute, and a shared pin needs no more.
|
|
15
|
+
* On Android the app's manifest must declare `ACCESS_COARSE_LOCATION` (and `ACCESS_FINE_LOCATION`
|
|
16
|
+
* for precise fixes); Capacitor's web view then asks the person for permission when this runs.
|
|
17
|
+
*/
|
|
18
|
+
export function currentPosition(timeout = 10_000): Promise<{ lat: number; lng: number }> {
|
|
19
|
+
return new Promise((resolve, reject) => {
|
|
20
|
+
if (typeof navigator === "undefined" || !navigator.geolocation) {
|
|
21
|
+
reject(new PositionError("unsupported"));
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
navigator.geolocation.getCurrentPosition(
|
|
25
|
+
(position) => resolve({ lat: position.coords.latitude, lng: position.coords.longitude }),
|
|
26
|
+
(error) => reject(new PositionError(REASONS[error.code] ?? "unavailable")),
|
|
27
|
+
{ enableHighAccuracy: false, timeout, maximumAge: 60_000 },
|
|
28
|
+
);
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** A link that opens a place in the maps app the person uses. */
|
|
33
|
+
export function mapsLink(lat: number, lng: number): string {
|
|
34
|
+
return `https://www.google.com/maps/search/?api=1&query=${lat},${lng}`;
|
|
35
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { WILAYAS } from "@/modules/gallery/composables/wilayas";
|
|
2
|
+
|
|
3
|
+
export type OrderStatus = "draft" | "confirmed" | "delivered";
|
|
4
|
+
|
|
5
|
+
export interface Order {
|
|
6
|
+
ref: string;
|
|
7
|
+
customer: string;
|
|
8
|
+
wilaya: string;
|
|
9
|
+
date: string;
|
|
10
|
+
status: OrderStatus;
|
|
11
|
+
totalCents: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const ORDER_COUNTS = [30, 1000, 5000] as const;
|
|
15
|
+
|
|
16
|
+
const STATUSES: OrderStatus[] = ["draft", "confirmed", "delivered"];
|
|
17
|
+
const CUSTOMERS = ["Oran Market", "Blida Gros", "Épicerie Saïd", "Annaba Fresh", "Tlemcen Dist."];
|
|
18
|
+
|
|
19
|
+
/** `count` orders, the same every time, so the table demo can be measured at any size. */
|
|
20
|
+
export function makeOrders(count: number): Order[] {
|
|
21
|
+
return Array.from({ length: count }, (_, index) => ({
|
|
22
|
+
ref: `SO-${1001 + index}`,
|
|
23
|
+
customer: CUSTOMERS[(index * 3) % CUSTOMERS.length]!,
|
|
24
|
+
wilaya: WILAYAS[(index * 11) % WILAYAS.length]!,
|
|
25
|
+
date: `2026-${String(1 + ((index >> 5) % 12)).padStart(2, "0")}-${String(1 + (index % 28)).padStart(2, "0")}`,
|
|
26
|
+
status: STATUSES[(index * 7) % STATUSES.length]!,
|
|
27
|
+
totalCents: ((index * 7919) % 90000) * 100 + 150000,
|
|
28
|
+
}));
|
|
29
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { MaterialShapeName } from "@cavulsqa/m3e";
|
|
2
|
+
import type { Component } from "vue";
|
|
3
|
+
import DistanceIcon from "~icons/material-symbols/distance-outline-rounded";
|
|
4
|
+
import DrinkIcon from "~icons/material-symbols/local-drink-outline-rounded";
|
|
5
|
+
import EventIcon from "~icons/material-symbols/event-available-outline-rounded";
|
|
6
|
+
import InventoryIcon from "~icons/material-symbols/inventory-2-outline-rounded";
|
|
7
|
+
import MapIcon from "~icons/material-symbols/map-outline-rounded";
|
|
8
|
+
import PinIcon from "~icons/material-symbols/pin-drop-rounded";
|
|
9
|
+
import ReceiptIcon from "~icons/material-symbols/receipt-long-outline-rounded";
|
|
10
|
+
import RouteIcon from "~icons/material-symbols/route-outline-rounded";
|
|
11
|
+
import ScheduleIcon from "~icons/material-symbols/schedule-outline-rounded";
|
|
12
|
+
import StoreIcon from "~icons/material-symbols/storefront-outline-rounded";
|
|
13
|
+
import WaterIcon from "~icons/material-symbols/water-drop-outline-rounded";
|
|
14
|
+
import type { Tone } from "@/shared/utils/tone";
|
|
15
|
+
|
|
16
|
+
export interface StoryFact {
|
|
17
|
+
id: string;
|
|
18
|
+
icon: Component;
|
|
19
|
+
value: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface FeaturedStory {
|
|
23
|
+
id: string;
|
|
24
|
+
tone: Tone;
|
|
25
|
+
shape: MaterialShapeName;
|
|
26
|
+
icon: Component;
|
|
27
|
+
facts: readonly StoryFact[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The cards of the container-transform demo. Each opens `/gallery/featured/<id>/`, and its tone is
|
|
32
|
+
* the colour the card hands to the page as it grows. Icons are imported once here and never made
|
|
33
|
+
* reactive, so they need no `markRaw`.
|
|
34
|
+
*/
|
|
35
|
+
export const featuredStories: readonly FeaturedStory[] = [
|
|
36
|
+
{
|
|
37
|
+
id: "range",
|
|
38
|
+
tone: "primary",
|
|
39
|
+
shape: "cookie9Sided",
|
|
40
|
+
icon: DrinkIcon,
|
|
41
|
+
facts: [
|
|
42
|
+
{ id: "products", icon: InventoryIcon, value: 12 },
|
|
43
|
+
{ id: "flavours", icon: WaterIcon, value: 4 },
|
|
44
|
+
{ id: "wilayas", icon: MapIcon, value: 9 },
|
|
45
|
+
],
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
id: "stores",
|
|
49
|
+
tone: "tertiary",
|
|
50
|
+
shape: "clover4Leaf",
|
|
51
|
+
icon: StoreIcon,
|
|
52
|
+
facts: [
|
|
53
|
+
{ id: "opened", icon: StoreIcon, value: 7 },
|
|
54
|
+
{ id: "visits", icon: EventIcon, value: 21 },
|
|
55
|
+
{ id: "orders", icon: ReceiptIcon, value: 15 },
|
|
56
|
+
],
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
id: "route",
|
|
60
|
+
tone: "secondary",
|
|
61
|
+
shape: "gem",
|
|
62
|
+
icon: RouteIcon,
|
|
63
|
+
facts: [
|
|
64
|
+
{ id: "stops", icon: PinIcon, value: 14 },
|
|
65
|
+
{ id: "distance", icon: DistanceIcon, value: 186 },
|
|
66
|
+
{ id: "hours", icon: ScheduleIcon, value: 9 },
|
|
67
|
+
],
|
|
68
|
+
},
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
export function findStory(id: string): FeaturedStory | undefined {
|
|
72
|
+
return featuredStories.find((story) => story.id === id);
|
|
73
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/** 0 to 4: how much a password resists guessing, by its length and the kinds of character in it. */
|
|
2
|
+
export function passwordScore(value: string): 0 | 1 | 2 | 3 | 4 {
|
|
3
|
+
if (value.length < 6) return 0;
|
|
4
|
+
const kinds = [/[a-z]/, /[A-Z]/, /\d/, /[^A-Za-z0-9]/].filter((kind) => kind.test(value)).length;
|
|
5
|
+
let score = (value.length >= 8 ? 1 : 0) + (value.length >= 12 ? 1 : 0) + Math.max(0, kinds - 1);
|
|
6
|
+
if (kinds === 1) score = Math.min(score, 1);
|
|
7
|
+
return Math.min(4, Math.max(1, score)) as 1 | 2 | 3 | 4;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The nine national digits of an Algerian mobile number from whatever was typed: spaces and dashes
|
|
12
|
+
* go, and so does a leading 0 or 213 dialling prefix, so "0555 12 34 56" and "+213 555123456" agree.
|
|
13
|
+
*/
|
|
14
|
+
export function mobileDigits(raw: string): string {
|
|
15
|
+
let digits = raw.replace(/\D/g, "");
|
|
16
|
+
if (digits.startsWith("213")) digits = digits.slice(3);
|
|
17
|
+
if (digits.startsWith("0")) digits = digits.slice(1);
|
|
18
|
+
return digits.slice(0, 9);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** The national digits grouped as they are read aloud: "5 55 12 34 56". */
|
|
22
|
+
export function formatMobile(digits: string): string {
|
|
23
|
+
const groups = [
|
|
24
|
+
digits.slice(0, 1),
|
|
25
|
+
digits.slice(1, 3),
|
|
26
|
+
digits.slice(3, 5),
|
|
27
|
+
digits.slice(5, 7),
|
|
28
|
+
digits.slice(7, 9),
|
|
29
|
+
];
|
|
30
|
+
return groups.filter((group) => group.length > 0).join(" ");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Whether nine national digits make a mobile number: it starts 5, 6 or 7. */
|
|
34
|
+
export function isMobile(digits: string): boolean {
|
|
35
|
+
return /^[567]\d{8}$/.test(digits);
|
|
36
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Capacitor } from "@capacitor/core";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Opens a link outside the app. On a device Capacitor hands any navigation to another host to the
|
|
5
|
+
* system, so a maps link opens the maps app and a web page the browser; in a browser it opens a new
|
|
6
|
+
* tab, so `vp dev` keeps the app it is running.
|
|
7
|
+
*/
|
|
8
|
+
export function openExternal(url: string): void {
|
|
9
|
+
if (Capacitor.isNativePlatform()) window.location.assign(url);
|
|
10
|
+
else window.open(url, "_blank", "noopener,noreferrer");
|
|
11
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { WILAYAS } from "@/modules/gallery/composables/wilayas";
|
|
2
|
+
|
|
3
|
+
export type VisitState = "done" | "next" | "planned";
|
|
4
|
+
|
|
5
|
+
export interface Visit {
|
|
6
|
+
id: string;
|
|
7
|
+
time: string;
|
|
8
|
+
/** Minutes since midnight. */
|
|
9
|
+
start: number;
|
|
10
|
+
end: number;
|
|
11
|
+
customer: string;
|
|
12
|
+
city: string;
|
|
13
|
+
state: VisitState;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const SHOPS = [
|
|
17
|
+
"Supérette El Feth",
|
|
18
|
+
"Alimentation Bachir",
|
|
19
|
+
"Market Ennour",
|
|
20
|
+
"Supérette Djamel",
|
|
21
|
+
"Épicerie Yacine",
|
|
22
|
+
"Mini-market Salam",
|
|
23
|
+
"Alimentation Rachid",
|
|
24
|
+
"Supérette El Baraka",
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
const WEEKEND = new Set([5, 6]);
|
|
28
|
+
|
|
29
|
+
function seed(day: string): number {
|
|
30
|
+
let hash = 0;
|
|
31
|
+
for (const char of day) hash = (hash * 31 + char.charCodeAt(0)) | 0;
|
|
32
|
+
return Math.abs(hash);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* A rep's visits for a day, the same every time for the same date, so the agenda demo needs no
|
|
37
|
+
* database. Friday and Saturday are the weekend. Past days are done; today, what ended before `now`
|
|
38
|
+
* (minutes since midnight) is done and the first one still running or ahead is next.
|
|
39
|
+
*/
|
|
40
|
+
export function visitsFor(day: string, today: string, now = 0): Visit[] {
|
|
41
|
+
const weekday = new Date(`${day}T00:00:00Z`).getUTCDay();
|
|
42
|
+
if (WEEKEND.has(weekday)) return [];
|
|
43
|
+
const base = seed(day);
|
|
44
|
+
const count = 3 + (base % 4);
|
|
45
|
+
const visits = Array.from({ length: count }, (_, index) => {
|
|
46
|
+
const minutes = 8 * 60 + index * 75 + ((base >> index) % 3) * 10;
|
|
47
|
+
const duration = 30 + ((base >> (index + 2)) % 4) * 15;
|
|
48
|
+
const time = `${String(Math.floor(minutes / 60)).padStart(2, "0")}:${String(minutes % 60).padStart(2, "0")}`;
|
|
49
|
+
return {
|
|
50
|
+
id: `${day}-${index}`,
|
|
51
|
+
time,
|
|
52
|
+
start: minutes,
|
|
53
|
+
end: minutes + duration,
|
|
54
|
+
customer: SHOPS[(base + index * 3) % SHOPS.length]!,
|
|
55
|
+
city: WILAYAS[(base + index * 7) % WILAYAS.length]!,
|
|
56
|
+
state: "planned" as VisitState,
|
|
57
|
+
};
|
|
58
|
+
});
|
|
59
|
+
const next = day === today ? visits.findIndex((visit) => visit.end > now) : -1;
|
|
60
|
+
for (const [index, visit] of visits.entries()) {
|
|
61
|
+
if (day < today || (day === today && (next === -1 || index < next))) visit.state = "done";
|
|
62
|
+
else if (index === next) visit.state = "next";
|
|
63
|
+
}
|
|
64
|
+
return visits;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** How many visits each day from `from` to `to`, both included, holds - the dots on a calendar. */
|
|
68
|
+
export function marksBetween(from: string, to: string, today: string): Record<string, number> {
|
|
69
|
+
const marks: Record<string, number> = {};
|
|
70
|
+
const last = Date.parse(`${to}T00:00:00Z`);
|
|
71
|
+
for (let time = Date.parse(`${from}T00:00:00Z`); time <= last; time += 86_400_000) {
|
|
72
|
+
const date = new Date(time).toISOString().slice(0, 10);
|
|
73
|
+
const count = visitsFor(date, today).length;
|
|
74
|
+
if (count > 0) marks[date] = count;
|
|
75
|
+
}
|
|
76
|
+
return marks;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** The visits on the weeks around `day`, for the week strip's dots. */
|
|
80
|
+
export function marksAround(day: string, today: string): Record<string, number> {
|
|
81
|
+
const centre = Date.parse(`${day}T00:00:00Z`);
|
|
82
|
+
const at = (offset: number) => new Date(centre + offset * 86_400_000).toISOString().slice(0, 10);
|
|
83
|
+
return marksBetween(at(-21), at(21), today);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** The visits on every day a month grid shows, its outside days included. */
|
|
87
|
+
export function marksForMonth(year: number, month: number, today: string): Record<string, number> {
|
|
88
|
+
const at = (day: number) => new Date(Date.UTC(year, month, day)).toISOString().slice(0, 10);
|
|
89
|
+
return marksBetween(at(-6), at(38), today);
|
|
90
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { formatIso, todayIso, type YearMonth } from "@cavulsqa/m3e-vue";
|
|
2
|
+
import { marksAround, marksForMonth, visitsFor } from "@/modules/gallery/composables/routePlan";
|
|
3
|
+
|
|
4
|
+
export const AGENDA_VIEWS = ["day", "week", "month"] as const;
|
|
5
|
+
export type AgendaView = (typeof AGENDA_VIEWS)[number];
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The route agenda's state: which view is on (remembered between visits), the chosen day, its
|
|
9
|
+
* visits, and the dots for whichever week or month is on screen. The month grid reports the month
|
|
10
|
+
* it turns to, so its dots follow the arrows rather than only the chosen day.
|
|
11
|
+
*/
|
|
12
|
+
export function useAgenda() {
|
|
13
|
+
const { t, locale } = useI18n();
|
|
14
|
+
const today = todayIso();
|
|
15
|
+
const day = ref(today);
|
|
16
|
+
const stored = useLocalStorage<AgendaView>("gallery.agenda.view", "week");
|
|
17
|
+
const view = computed<AgendaView>({
|
|
18
|
+
get: () => (AGENDA_VIEWS.includes(stored.value) ? stored.value : "week"),
|
|
19
|
+
set: (next) => (stored.value = next),
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const shownMonth = shallowRef<YearMonth>(monthOf(today));
|
|
23
|
+
watch(day, (next) => (shownMonth.value = monthOf(next)));
|
|
24
|
+
|
|
25
|
+
const clock = useNow({ interval: 60_000 });
|
|
26
|
+
const visits = computed(() =>
|
|
27
|
+
visitsFor(day.value, today, clock.value.getHours() * 60 + clock.value.getMinutes()),
|
|
28
|
+
);
|
|
29
|
+
const weekMarks = computed(() => marksAround(day.value, today));
|
|
30
|
+
const monthMarks = computed(() =>
|
|
31
|
+
marksForMonth(shownMonth.value.year, shownMonth.value.month, today),
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
const month = computed(() =>
|
|
35
|
+
formatIso(day.value, locale.value, { month: "long", year: "numeric" }),
|
|
36
|
+
);
|
|
37
|
+
const heading = computed(() =>
|
|
38
|
+
formatIso(day.value, locale.value, { weekday: "long", day: "numeric", month: "long" }),
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
function visitsLabel(count: number) {
|
|
42
|
+
return t("gallery.agenda.visits", { count }, count);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
today,
|
|
47
|
+
day,
|
|
48
|
+
view,
|
|
49
|
+
visits,
|
|
50
|
+
weekMarks,
|
|
51
|
+
monthMarks,
|
|
52
|
+
shownMonth,
|
|
53
|
+
month,
|
|
54
|
+
heading,
|
|
55
|
+
visitsLabel,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function monthOf(iso: string): YearMonth {
|
|
60
|
+
return { year: Number(iso.slice(0, 4)), month: Number(iso.slice(5, 7)) - 1 };
|
|
61
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { listCustomers } from "@/domains/sales/sales.repository";
|
|
2
|
+
import { getDatabase } from "@/shared/database/database";
|
|
3
|
+
import { useReactiveQuery } from "@/shared/database/queries";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The customers a chat can share as contacts. Deferred until `wanted` first turns true, so opening
|
|
7
|
+
* the conversation does not spend the database thread on a list nobody may ask for; after that it
|
|
8
|
+
* stays live, and a customer added elsewhere shows up in the picker.
|
|
9
|
+
*/
|
|
10
|
+
export function useChatCustomers(wanted: Readonly<Ref<boolean>>) {
|
|
11
|
+
const asked = shallowRef(false);
|
|
12
|
+
watch(
|
|
13
|
+
wanted,
|
|
14
|
+
(value) => {
|
|
15
|
+
if (value) asked.value = true;
|
|
16
|
+
},
|
|
17
|
+
{ immediate: true },
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
const query = useReactiveQuery(() => listCustomers(getDatabase().db), {
|
|
21
|
+
tables: ["customer"],
|
|
22
|
+
queryKey: ["gallery:customers"],
|
|
23
|
+
enabled: asked,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
customers: computed(() => query.data.value ?? []),
|
|
28
|
+
loading: query.loading,
|
|
29
|
+
failed: computed(() => query.error.value !== null),
|
|
30
|
+
};
|
|
31
|
+
}
|