Добавлен интерфейс технического обслуживания устройств
This commit is contained in:
parent
ee339bf301
commit
4cf0b0d542
|
|
@ -0,0 +1,2 @@
|
||||||
|
# Separate repository
|
||||||
|
/Arma_mdm/
|
||||||
|
|
@ -12,6 +12,16 @@ dist
|
||||||
dist-ssr
|
dist-ssr
|
||||||
*.local
|
*.local
|
||||||
|
|
||||||
|
# Local environment
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
|
||||||
|
# Local working files
|
||||||
|
.artifact-third-sheet-work/
|
||||||
|
/*.xlsx
|
||||||
|
/public/photo_2025-03-29_20-05-51.jpg
|
||||||
|
|
||||||
# Editor directories and files
|
# Editor directories and files
|
||||||
.vscode/*
|
.vscode/*
|
||||||
!.vscode/extensions.json
|
!.vscode/extensions.json
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
import type { CodegenConfig } from '@graphql-codegen/cli'
|
||||||
|
|
||||||
|
const config: CodegenConfig = {
|
||||||
|
schema: '../Arma_mdm/src/main/resources/graphql/**/*.graphql',
|
||||||
|
documents: ['src/**/*.graphql'],
|
||||||
|
generates: {
|
||||||
|
'src/shared/api/generated/': {
|
||||||
|
preset: 'client',
|
||||||
|
config: {
|
||||||
|
enumsAsTypes: true,
|
||||||
|
scalars: {
|
||||||
|
ID: {
|
||||||
|
input: 'string',
|
||||||
|
output: 'number',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
useTypeImports: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ignoreNoDocuments: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default config
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -4,8 +4,10 @@
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite --host 192.168.1.181",
|
"dev": "vite --host 192.168.1.91",
|
||||||
"build": "tsc -b && vite build",
|
"build": "tsc -b && vite build",
|
||||||
|
"codegen": "graphql-codegen --config codegen.ts",
|
||||||
|
"codegen:watch": "graphql-codegen --config codegen.ts --watch",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
|
|
@ -41,6 +43,8 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^10.0.1",
|
"@eslint/js": "^10.0.1",
|
||||||
|
"@graphql-codegen/cli": "^7.2.0",
|
||||||
|
"@graphql-codegen/client-preset": "^6.1.3",
|
||||||
"@types/leaflet": "^1.9.21",
|
"@types/leaflet": "^1.9.21",
|
||||||
"@types/node": "^24.12.2",
|
"@types/node": "^24.12.2",
|
||||||
"@types/react": "^19.2.14",
|
"@types/react": "^19.2.14",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,471 @@
|
||||||
|
query GetPhones(
|
||||||
|
$page: Int!
|
||||||
|
$query: String
|
||||||
|
$locked: Boolean
|
||||||
|
$needMaintenance: Boolean
|
||||||
|
$networkStatus: [PhoneNetworkStatus!]
|
||||||
|
$technicalStatus: [PhoneTechnicalStatus!]
|
||||||
|
$orgs: [ID!]
|
||||||
|
$sortDirection: SortDirection!
|
||||||
|
$sortField: PhoneSortField!
|
||||||
|
) {
|
||||||
|
getPhones(
|
||||||
|
page: $page
|
||||||
|
query: $query
|
||||||
|
locked: $locked
|
||||||
|
needMaintenance: $needMaintenance
|
||||||
|
networkStatus: $networkStatus
|
||||||
|
technicalStatus: $technicalStatus
|
||||||
|
orgs: $orgs
|
||||||
|
sortDirection: $sortDirection
|
||||||
|
sortField: $sortField
|
||||||
|
) {
|
||||||
|
page {
|
||||||
|
id
|
||||||
|
imei
|
||||||
|
imei2
|
||||||
|
serial
|
||||||
|
networkStatus
|
||||||
|
|
||||||
|
lastLocation {
|
||||||
|
alt
|
||||||
|
date
|
||||||
|
lat
|
||||||
|
lng
|
||||||
|
}
|
||||||
|
|
||||||
|
org {
|
||||||
|
id
|
||||||
|
logoUrl
|
||||||
|
name
|
||||||
|
|
||||||
|
policy {
|
||||||
|
bluetooth
|
||||||
|
bluetoothEditable
|
||||||
|
camera
|
||||||
|
cameraEditable
|
||||||
|
GPS
|
||||||
|
gpsEditable
|
||||||
|
sim
|
||||||
|
simEditable
|
||||||
|
offlineTimeThreshold
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
orgId
|
||||||
|
|
||||||
|
policy {
|
||||||
|
bluetooth
|
||||||
|
camera
|
||||||
|
GPS
|
||||||
|
sim
|
||||||
|
locked
|
||||||
|
}
|
||||||
|
|
||||||
|
techState {
|
||||||
|
batteryLevel
|
||||||
|
malfunctions
|
||||||
|
malfunctionComment
|
||||||
|
needMaintenance
|
||||||
|
status
|
||||||
|
temperature
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
totalElements
|
||||||
|
totalPages
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
query GetPhonesTabsStats($query: String, $orgs: [ID!]) {
|
||||||
|
totalPhones: getPhones(
|
||||||
|
page: 0
|
||||||
|
query: $query
|
||||||
|
orgs: $orgs
|
||||||
|
sortDirection: DESC
|
||||||
|
sortField: ID
|
||||||
|
) {
|
||||||
|
totalElements
|
||||||
|
}
|
||||||
|
|
||||||
|
onlinePhones: getPhones(
|
||||||
|
page: 0
|
||||||
|
query: $query
|
||||||
|
networkStatus: [Online]
|
||||||
|
orgs: $orgs
|
||||||
|
sortDirection: DESC
|
||||||
|
sortField: ID
|
||||||
|
) {
|
||||||
|
totalElements
|
||||||
|
}
|
||||||
|
|
||||||
|
lockedPhones: getPhones(
|
||||||
|
page: 0
|
||||||
|
query: $query
|
||||||
|
locked: true
|
||||||
|
orgs: $orgs
|
||||||
|
sortDirection: DESC
|
||||||
|
sortField: ID
|
||||||
|
) {
|
||||||
|
totalElements
|
||||||
|
}
|
||||||
|
|
||||||
|
lostPhones: getPhones(
|
||||||
|
page: 0
|
||||||
|
query: $query
|
||||||
|
networkStatus: [Lost]
|
||||||
|
orgs: $orgs
|
||||||
|
sortDirection: DESC
|
||||||
|
sortField: ID
|
||||||
|
) {
|
||||||
|
totalElements
|
||||||
|
}
|
||||||
|
|
||||||
|
maintenancePhones: getPhones(
|
||||||
|
page: 0
|
||||||
|
query: $query
|
||||||
|
technicalStatus: [NeedsMaintenance]
|
||||||
|
orgs: $orgs
|
||||||
|
sortDirection: DESC
|
||||||
|
sortField: ID
|
||||||
|
) {
|
||||||
|
totalElements
|
||||||
|
}
|
||||||
|
|
||||||
|
servicePhones: getPhones(
|
||||||
|
page: 0
|
||||||
|
query: $query
|
||||||
|
technicalStatus: [InService]
|
||||||
|
orgs: $orgs
|
||||||
|
sortDirection: DESC
|
||||||
|
sortField: ID
|
||||||
|
) {
|
||||||
|
totalElements
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
query GetPhone($id: Int!) {
|
||||||
|
getPhone(id: $id) {
|
||||||
|
id
|
||||||
|
imei
|
||||||
|
imei2
|
||||||
|
networkStatus
|
||||||
|
|
||||||
|
lastLocation {
|
||||||
|
alt
|
||||||
|
date
|
||||||
|
lat
|
||||||
|
lng
|
||||||
|
}
|
||||||
|
|
||||||
|
org {
|
||||||
|
creationDate
|
||||||
|
id
|
||||||
|
name
|
||||||
|
policy {
|
||||||
|
bluetooth
|
||||||
|
bluetoothEditable
|
||||||
|
camera
|
||||||
|
cameraEditable
|
||||||
|
GPS
|
||||||
|
gpsEditable
|
||||||
|
sim
|
||||||
|
simEditable
|
||||||
|
offlineTimeThreshold
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
orgId
|
||||||
|
|
||||||
|
policy {
|
||||||
|
bluetooth
|
||||||
|
camera
|
||||||
|
GPS
|
||||||
|
sim
|
||||||
|
locked
|
||||||
|
}
|
||||||
|
|
||||||
|
registerDate
|
||||||
|
serial
|
||||||
|
|
||||||
|
techState {
|
||||||
|
batteryCycles
|
||||||
|
batteryLevel
|
||||||
|
batteryRemainingCapacity
|
||||||
|
hits
|
||||||
|
malfunctions
|
||||||
|
malfunctionComment
|
||||||
|
needMaintenance
|
||||||
|
overheats
|
||||||
|
status
|
||||||
|
temperature
|
||||||
|
worktime
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
query GetPhoneGpsTrack($phoneId: ID!, $startDate: Float!, $endDate: Float) {
|
||||||
|
getPhoneGpsTrack(phoneId: $phoneId, startDate: $startDate, endDate: $endDate) {
|
||||||
|
alt
|
||||||
|
date
|
||||||
|
lat
|
||||||
|
lng
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
query GetPhonePackages($phoneId: ID!) {
|
||||||
|
getPhonePackages(phoneId: $phoneId) {
|
||||||
|
category
|
||||||
|
iconUrl
|
||||||
|
installDate
|
||||||
|
name
|
||||||
|
packageName
|
||||||
|
version
|
||||||
|
versionName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
query GetTelemetry(
|
||||||
|
$phoneId: Int!
|
||||||
|
$packagesPhoneId: ID!
|
||||||
|
$startDate: Float!
|
||||||
|
$endDate: Float
|
||||||
|
) {
|
||||||
|
getTelemetry(phoneId: $phoneId, startDate: $startDate, endDate: $endDate) {
|
||||||
|
batteryCapacity
|
||||||
|
batteryCycles
|
||||||
|
batteryLevel
|
||||||
|
date
|
||||||
|
temperature
|
||||||
|
}
|
||||||
|
|
||||||
|
getPhoneStateEvents(phoneId: $packagesPhoneId, startDate: $startDate, endDate: $endDate) {
|
||||||
|
date
|
||||||
|
phoneId
|
||||||
|
type
|
||||||
|
}
|
||||||
|
|
||||||
|
getPhonePackagesUseEvents(
|
||||||
|
phoneId: $packagesPhoneId
|
||||||
|
startDate: $startDate
|
||||||
|
endDate: $endDate
|
||||||
|
) {
|
||||||
|
events {
|
||||||
|
type
|
||||||
|
packageName
|
||||||
|
date
|
||||||
|
}
|
||||||
|
|
||||||
|
phonePackage {
|
||||||
|
category
|
||||||
|
iconUrl
|
||||||
|
installDate
|
||||||
|
name
|
||||||
|
packageName
|
||||||
|
version
|
||||||
|
versionName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
query GetDevicePage(
|
||||||
|
$id: Int!
|
||||||
|
$phoneId: ID!
|
||||||
|
$telemetryStartDate: Float!
|
||||||
|
$telemetryEndDate: Float
|
||||||
|
$gpsStartDate: Float!
|
||||||
|
$gpsEndDate: Float
|
||||||
|
) {
|
||||||
|
getPhone(id: $id) {
|
||||||
|
id
|
||||||
|
imei
|
||||||
|
imei2
|
||||||
|
networkStatus
|
||||||
|
|
||||||
|
lastLocation {
|
||||||
|
alt
|
||||||
|
date
|
||||||
|
lat
|
||||||
|
lng
|
||||||
|
}
|
||||||
|
|
||||||
|
org {
|
||||||
|
creationDate
|
||||||
|
id
|
||||||
|
name
|
||||||
|
policy {
|
||||||
|
bluetooth
|
||||||
|
bluetoothEditable
|
||||||
|
camera
|
||||||
|
cameraEditable
|
||||||
|
GPS
|
||||||
|
gpsEditable
|
||||||
|
sim
|
||||||
|
simEditable
|
||||||
|
offlineTimeThreshold
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
orgId
|
||||||
|
|
||||||
|
policy {
|
||||||
|
bluetooth
|
||||||
|
camera
|
||||||
|
GPS
|
||||||
|
sim
|
||||||
|
locked
|
||||||
|
}
|
||||||
|
|
||||||
|
registerDate
|
||||||
|
serial
|
||||||
|
|
||||||
|
techState {
|
||||||
|
batteryCycles
|
||||||
|
batteryLevel
|
||||||
|
batteryRemainingCapacity
|
||||||
|
hits
|
||||||
|
malfunctions
|
||||||
|
malfunctionComment
|
||||||
|
needMaintenance
|
||||||
|
overheats
|
||||||
|
status
|
||||||
|
temperature
|
||||||
|
worktime
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getTelemetry(
|
||||||
|
phoneId: $id
|
||||||
|
startDate: $telemetryStartDate
|
||||||
|
endDate: $telemetryEndDate
|
||||||
|
) {
|
||||||
|
batteryCapacity
|
||||||
|
batteryCycles
|
||||||
|
batteryLevel
|
||||||
|
date
|
||||||
|
temperature
|
||||||
|
}
|
||||||
|
|
||||||
|
getPhoneGpsTrack(
|
||||||
|
phoneId: $phoneId
|
||||||
|
startDate: $gpsStartDate
|
||||||
|
endDate: $gpsEndDate
|
||||||
|
) {
|
||||||
|
alt
|
||||||
|
date
|
||||||
|
lat
|
||||||
|
lng
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mutation CreatePhoneRegistrationToken {
|
||||||
|
createPhoneRegistrationToken {
|
||||||
|
address
|
||||||
|
expiresIn
|
||||||
|
token
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mutation ChangePhonePolicy(
|
||||||
|
$id: ID!
|
||||||
|
$bluetooth: ServiceUseOption!
|
||||||
|
$camera: ComponentUseOption!
|
||||||
|
$GPS: ServiceUseOption!
|
||||||
|
$sim: ServiceUseOption!
|
||||||
|
$locked: Boolean!
|
||||||
|
) {
|
||||||
|
changePhonePolicy(
|
||||||
|
id: $id
|
||||||
|
policy: {
|
||||||
|
bluetooth: $bluetooth
|
||||||
|
camera: $camera
|
||||||
|
GPS: $GPS
|
||||||
|
sim: $sim
|
||||||
|
locked: $locked
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
bluetooth
|
||||||
|
camera
|
||||||
|
GPS
|
||||||
|
sim
|
||||||
|
locked
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
query GetActivePhoneMaintenanceCase($phoneId: ID!) {
|
||||||
|
getActivePhoneMaintenanceCase(phoneId: $phoneId) {
|
||||||
|
id
|
||||||
|
phoneId
|
||||||
|
status
|
||||||
|
malfunctions
|
||||||
|
comment
|
||||||
|
detectedAt
|
||||||
|
serviceStartedAt
|
||||||
|
closedAt
|
||||||
|
|
||||||
|
metrics {
|
||||||
|
hits
|
||||||
|
overheats
|
||||||
|
batteryCycles
|
||||||
|
worktime
|
||||||
|
batteryCapacity
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
query GetPhoneMaintenanceHistory($phoneId: ID!) {
|
||||||
|
getPhoneMaintenanceHistory(phoneId: $phoneId) {
|
||||||
|
id
|
||||||
|
phoneId
|
||||||
|
status
|
||||||
|
malfunctions
|
||||||
|
comment
|
||||||
|
detectedAt
|
||||||
|
serviceStartedAt
|
||||||
|
closedAt
|
||||||
|
|
||||||
|
metrics {
|
||||||
|
hits
|
||||||
|
overheats
|
||||||
|
batteryCycles
|
||||||
|
worktime
|
||||||
|
batteryCapacity
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mutation SetPhoneMaintenanceRequired(
|
||||||
|
$id: ID!
|
||||||
|
$malfunctions: [PhoneMalfunction!]!
|
||||||
|
$comment: String
|
||||||
|
) {
|
||||||
|
setPhoneMaintenanceRequired(
|
||||||
|
id: $id
|
||||||
|
form: {
|
||||||
|
malfunctions: $malfunctions
|
||||||
|
comment: $comment
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
mutation MarkPhoneHealthy($id: ID!) {
|
||||||
|
markPhoneHealthy(id: $id)
|
||||||
|
}
|
||||||
|
|
||||||
|
mutation SendPhoneToMaintenance(
|
||||||
|
$id: ID!
|
||||||
|
$malfunctions: [PhoneMalfunction!]!
|
||||||
|
$comment: String
|
||||||
|
) {
|
||||||
|
sendPhoneToMaintenance(
|
||||||
|
id: $id
|
||||||
|
form: {
|
||||||
|
malfunctions: $malfunctions
|
||||||
|
comment: $comment
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
mutation ReturnPhoneFromMaintenance($id: ID!) {
|
||||||
|
returnPhoneFromMaintenance(id: $id)
|
||||||
|
}
|
||||||
|
|
@ -1,390 +1,17 @@
|
||||||
import { gql } from '@apollo/client'
|
export {
|
||||||
|
GetPhonesDocument as GET_PHONES_QUERY,
|
||||||
export const GET_PHONES_QUERY = gql`
|
GetPhonesTabsStatsDocument as GET_PHONES_TABS_STATS_QUERY,
|
||||||
query GetPhones(
|
GetPhoneDocument as GET_PHONE_QUERY,
|
||||||
$page: Int!
|
GetPhoneGpsTrackDocument as GET_PHONE_GPS_TRACK_QUERY,
|
||||||
$query: String
|
GetPhonePackagesDocument as GET_PHONE_PACKAGES_QUERY,
|
||||||
$locked: Boolean
|
GetTelemetryDocument as GET_TELEMETRY_QUERY,
|
||||||
$needMaintenance: Boolean
|
GetDevicePageDocument as GET_DEVICE_PAGE_QUERY,
|
||||||
$networkStatus: [PhoneNetworkStatus!]
|
CreatePhoneRegistrationTokenDocument as CREATE_PHONE_REGISTRATION_TOKEN_MUTATION,
|
||||||
$orgs: [ID!]
|
ChangePhonePolicyDocument as CHANGE_PHONE_POLICY_MUTATION,
|
||||||
$sortDirection: SortDirection!
|
GetActivePhoneMaintenanceCaseDocument as GET_ACTIVE_PHONE_MAINTENANCE_CASE_QUERY,
|
||||||
$sortField: PhoneSortField!
|
GetPhoneMaintenanceHistoryDocument as GET_PHONE_MAINTENANCE_HISTORY_QUERY,
|
||||||
) {
|
SetPhoneMaintenanceRequiredDocument as SET_PHONE_MAINTENANCE_REQUIRED_MUTATION,
|
||||||
getPhones(
|
MarkPhoneHealthyDocument as MARK_PHONE_HEALTHY_MUTATION,
|
||||||
page: $page
|
SendPhoneToMaintenanceDocument as SEND_PHONE_TO_MAINTENANCE_MUTATION,
|
||||||
query: $query
|
ReturnPhoneFromMaintenanceDocument as RETURN_PHONE_FROM_MAINTENANCE_MUTATION,
|
||||||
locked: $locked
|
} from '../../../shared/api/generated/graphql'
|
||||||
needMaintenance: $needMaintenance
|
|
||||||
networkStatus: $networkStatus
|
|
||||||
orgs: $orgs
|
|
||||||
sortDirection: $sortDirection
|
|
||||||
sortField: $sortField
|
|
||||||
) {
|
|
||||||
page {
|
|
||||||
id
|
|
||||||
imei
|
|
||||||
imei2
|
|
||||||
serial
|
|
||||||
networkStatus
|
|
||||||
|
|
||||||
lastLocation {
|
|
||||||
alt
|
|
||||||
date
|
|
||||||
lat
|
|
||||||
lng
|
|
||||||
}
|
|
||||||
|
|
||||||
org {
|
|
||||||
id
|
|
||||||
logoUrl
|
|
||||||
name
|
|
||||||
|
|
||||||
policy {
|
|
||||||
bluetooth
|
|
||||||
bluetoothEditable
|
|
||||||
camera
|
|
||||||
cameraEditable
|
|
||||||
GPS
|
|
||||||
gpsEditable
|
|
||||||
sim
|
|
||||||
simEditable
|
|
||||||
offlineTimeThreshold
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
orgId
|
|
||||||
|
|
||||||
policy {
|
|
||||||
bluetooth
|
|
||||||
camera
|
|
||||||
GPS
|
|
||||||
sim
|
|
||||||
locked
|
|
||||||
}
|
|
||||||
|
|
||||||
techState {
|
|
||||||
batteryLevel
|
|
||||||
needMaintenance
|
|
||||||
temperature
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
totalElements
|
|
||||||
totalPages
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
export const GET_PHONES_TABS_STATS_QUERY = gql`
|
|
||||||
query GetPhonesTabsStats($query: String, $orgs: [ID!]) {
|
|
||||||
totalPhones: getPhones(
|
|
||||||
page: 0
|
|
||||||
query: $query
|
|
||||||
orgs: $orgs
|
|
||||||
sortDirection: DESC
|
|
||||||
sortField: ID
|
|
||||||
) {
|
|
||||||
totalElements
|
|
||||||
}
|
|
||||||
|
|
||||||
onlinePhones: getPhones(
|
|
||||||
page: 0
|
|
||||||
query: $query
|
|
||||||
networkStatus: [Online]
|
|
||||||
orgs: $orgs
|
|
||||||
sortDirection: DESC
|
|
||||||
sortField: ID
|
|
||||||
) {
|
|
||||||
totalElements
|
|
||||||
}
|
|
||||||
|
|
||||||
lockedPhones: getPhones(
|
|
||||||
page: 0
|
|
||||||
query: $query
|
|
||||||
locked: true
|
|
||||||
orgs: $orgs
|
|
||||||
sortDirection: DESC
|
|
||||||
sortField: ID
|
|
||||||
) {
|
|
||||||
totalElements
|
|
||||||
}
|
|
||||||
|
|
||||||
lostPhones: getPhones(
|
|
||||||
page: 0
|
|
||||||
query: $query
|
|
||||||
networkStatus: [Lost]
|
|
||||||
orgs: $orgs
|
|
||||||
sortDirection: DESC
|
|
||||||
sortField: ID
|
|
||||||
) {
|
|
||||||
totalElements
|
|
||||||
}
|
|
||||||
|
|
||||||
maintenancePhones: getPhones(
|
|
||||||
page: 0
|
|
||||||
query: $query
|
|
||||||
needMaintenance: true
|
|
||||||
orgs: $orgs
|
|
||||||
sortDirection: DESC
|
|
||||||
sortField: ID
|
|
||||||
) {
|
|
||||||
totalElements
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
export const GET_PHONE_QUERY = gql`
|
|
||||||
query GetPhone($id: Int!) {
|
|
||||||
getPhone(id: $id) {
|
|
||||||
id
|
|
||||||
imei
|
|
||||||
imei2
|
|
||||||
networkStatus
|
|
||||||
|
|
||||||
lastLocation {
|
|
||||||
alt
|
|
||||||
date
|
|
||||||
lat
|
|
||||||
lng
|
|
||||||
}
|
|
||||||
|
|
||||||
org {
|
|
||||||
creationDate
|
|
||||||
id
|
|
||||||
name
|
|
||||||
policy {
|
|
||||||
bluetooth
|
|
||||||
bluetoothEditable
|
|
||||||
camera
|
|
||||||
cameraEditable
|
|
||||||
GPS
|
|
||||||
gpsEditable
|
|
||||||
sim
|
|
||||||
simEditable
|
|
||||||
offlineTimeThreshold
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
orgId
|
|
||||||
|
|
||||||
policy {
|
|
||||||
bluetooth
|
|
||||||
camera
|
|
||||||
GPS
|
|
||||||
sim
|
|
||||||
locked
|
|
||||||
}
|
|
||||||
|
|
||||||
registerDate
|
|
||||||
serial
|
|
||||||
|
|
||||||
techState {
|
|
||||||
batteryCycles
|
|
||||||
batteryLevel
|
|
||||||
batteryRemainingCapacity
|
|
||||||
hits
|
|
||||||
needMaintenance
|
|
||||||
overheats
|
|
||||||
worktime
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
export const GET_PHONE_GPS_TRACK_QUERY = gql`
|
|
||||||
query GetPhoneGpsTrack($phoneId: ID!, $startDate: Float!, $endDate: Float) {
|
|
||||||
getPhoneGpsTrack(phoneId: $phoneId, startDate: $startDate, endDate: $endDate) {
|
|
||||||
alt
|
|
||||||
date
|
|
||||||
lat
|
|
||||||
lng
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
export const GET_PHONE_PACKAGES_QUERY = gql`
|
|
||||||
query GetPhonePackages($phoneId: ID!) {
|
|
||||||
getPhonePackages(phoneId: $phoneId) {
|
|
||||||
category
|
|
||||||
iconUrl
|
|
||||||
installDate
|
|
||||||
name
|
|
||||||
packageName
|
|
||||||
version
|
|
||||||
versionName
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
export const GET_TELEMETRY_QUERY = gql`
|
|
||||||
query GetTelemetry(
|
|
||||||
$phoneId: Int!
|
|
||||||
$packagesPhoneId: ID!
|
|
||||||
$startDate: Float!
|
|
||||||
$endDate: Float
|
|
||||||
) {
|
|
||||||
getTelemetry(phoneId: $phoneId, startDate: $startDate, endDate: $endDate) {
|
|
||||||
batteryCapacity
|
|
||||||
batteryCycles
|
|
||||||
batteryLevel
|
|
||||||
date
|
|
||||||
temperature
|
|
||||||
}
|
|
||||||
|
|
||||||
getPhoneStateEvents(phoneId: $packagesPhoneId, startDate: $startDate, endDate: $endDate) {
|
|
||||||
date
|
|
||||||
phoneId
|
|
||||||
type
|
|
||||||
}
|
|
||||||
|
|
||||||
getPhonePackagesUseEvents(
|
|
||||||
phoneId: $packagesPhoneId
|
|
||||||
startDate: $startDate
|
|
||||||
endDate: $endDate
|
|
||||||
) {
|
|
||||||
events {
|
|
||||||
type
|
|
||||||
packageName
|
|
||||||
date
|
|
||||||
}
|
|
||||||
|
|
||||||
phonePackage {
|
|
||||||
category
|
|
||||||
iconUrl
|
|
||||||
installDate
|
|
||||||
name
|
|
||||||
packageName
|
|
||||||
version
|
|
||||||
versionName
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
export const GET_DEVICE_PAGE_QUERY = gql`
|
|
||||||
query GetDevicePage(
|
|
||||||
$id: Int!
|
|
||||||
$phoneId: ID!
|
|
||||||
$telemetryStartDate: Float!
|
|
||||||
$telemetryEndDate: Float
|
|
||||||
$gpsStartDate: Float!
|
|
||||||
$gpsEndDate: Float
|
|
||||||
) {
|
|
||||||
getPhone(id: $id) {
|
|
||||||
id
|
|
||||||
imei
|
|
||||||
imei2
|
|
||||||
networkStatus
|
|
||||||
|
|
||||||
lastLocation {
|
|
||||||
alt
|
|
||||||
date
|
|
||||||
lat
|
|
||||||
lng
|
|
||||||
}
|
|
||||||
|
|
||||||
org {
|
|
||||||
creationDate
|
|
||||||
id
|
|
||||||
name
|
|
||||||
policy {
|
|
||||||
bluetooth
|
|
||||||
bluetoothEditable
|
|
||||||
camera
|
|
||||||
cameraEditable
|
|
||||||
GPS
|
|
||||||
gpsEditable
|
|
||||||
sim
|
|
||||||
simEditable
|
|
||||||
offlineTimeThreshold
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
orgId
|
|
||||||
|
|
||||||
policy {
|
|
||||||
bluetooth
|
|
||||||
camera
|
|
||||||
GPS
|
|
||||||
sim
|
|
||||||
locked
|
|
||||||
}
|
|
||||||
|
|
||||||
registerDate
|
|
||||||
serial
|
|
||||||
|
|
||||||
techState {
|
|
||||||
batteryCycles
|
|
||||||
batteryLevel
|
|
||||||
batteryRemainingCapacity
|
|
||||||
hits
|
|
||||||
needMaintenance
|
|
||||||
overheats
|
|
||||||
temperature
|
|
||||||
worktime
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getTelemetry(
|
|
||||||
phoneId: $id
|
|
||||||
startDate: $telemetryStartDate
|
|
||||||
endDate: $telemetryEndDate
|
|
||||||
) {
|
|
||||||
batteryCapacity
|
|
||||||
batteryCycles
|
|
||||||
batteryLevel
|
|
||||||
date
|
|
||||||
temperature
|
|
||||||
}
|
|
||||||
|
|
||||||
getPhoneGpsTrack(
|
|
||||||
phoneId: $phoneId
|
|
||||||
startDate: $gpsStartDate
|
|
||||||
endDate: $gpsEndDate
|
|
||||||
) {
|
|
||||||
alt
|
|
||||||
date
|
|
||||||
lat
|
|
||||||
lng
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
export const CREATE_PHONE_REGISTRATION_TOKEN_MUTATION = gql`
|
|
||||||
mutation CreatePhoneRegistrationToken {
|
|
||||||
createPhoneRegistrationToken {
|
|
||||||
address
|
|
||||||
expiresIn
|
|
||||||
token
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
export const CHANGE_PHONE_POLICY_MUTATION = gql`
|
|
||||||
mutation ChangePhonePolicy(
|
|
||||||
$id: ID!
|
|
||||||
$bluetooth: ServiceUseOption!
|
|
||||||
$camera: ComponentUseOption!
|
|
||||||
$GPS: ServiceUseOption!
|
|
||||||
$sim: ServiceUseOption!
|
|
||||||
$locked: Boolean!
|
|
||||||
) {
|
|
||||||
changePhonePolicy(
|
|
||||||
id: $id
|
|
||||||
policy: {
|
|
||||||
bluetooth: $bluetooth
|
|
||||||
camera: $camera
|
|
||||||
GPS: $GPS
|
|
||||||
sim: $sim
|
|
||||||
locked: $locked
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
bluetooth
|
|
||||||
camera
|
|
||||||
GPS
|
|
||||||
sim
|
|
||||||
locked
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
import type {
|
||||||
|
DeviceMalfunction,
|
||||||
|
DeviceTechnicalStatus,
|
||||||
|
} from '../model/types'
|
||||||
|
|
||||||
|
export const deviceMalfunctionLabels: Record<DeviceMalfunction, string> = {
|
||||||
|
LowBatteryCapacity: 'Низкая ёмкость АКБ',
|
||||||
|
CriticalHits: 'Критические удары',
|
||||||
|
CriticalChargeCycles: 'Критические циклы зарядки',
|
||||||
|
DoesNotTurnOn: 'Не включается',
|
||||||
|
Other: 'Другое',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deviceTechnicalStatusLabels: Record<
|
||||||
|
DeviceTechnicalStatus,
|
||||||
|
string
|
||||||
|
> = {
|
||||||
|
Healthy: 'Исправно',
|
||||||
|
NeedsMaintenance: 'Требует ТО',
|
||||||
|
InService: 'На обслуживании',
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDeviceMalfunctionSummary(
|
||||||
|
malfunctions: DeviceMalfunction[] = [],
|
||||||
|
comment?: string | null,
|
||||||
|
) {
|
||||||
|
const details = malfunctions.map((item) => deviceMalfunctionLabels[item])
|
||||||
|
const normalizedDetails = new Set(
|
||||||
|
details.map((item) => item.trim().toLocaleLowerCase('ru-RU')),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (
|
||||||
|
comment?.trim() &&
|
||||||
|
!normalizedDetails.has(comment.trim().toLocaleLowerCase('ru-RU'))
|
||||||
|
) {
|
||||||
|
details.push(comment.trim())
|
||||||
|
}
|
||||||
|
|
||||||
|
return details.join('. ')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isRedundantMalfunctionComment(
|
||||||
|
malfunctions: DeviceMalfunction[] = [],
|
||||||
|
comment?: string | null,
|
||||||
|
) {
|
||||||
|
if (!comment?.trim()) return true
|
||||||
|
|
||||||
|
const normalizedComment = comment.trim().toLocaleLowerCase('ru-RU')
|
||||||
|
return malfunctions.some(
|
||||||
|
(item) =>
|
||||||
|
deviceMalfunctionLabels[item].trim().toLocaleLowerCase('ru-RU') ===
|
||||||
|
normalizedComment,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,42 @@
|
||||||
|
import type {
|
||||||
|
ChangePhonePolicyMutation,
|
||||||
|
ChangePhonePolicyMutationVariables,
|
||||||
|
ComponentUseOption as GeneratedComponentUseOption,
|
||||||
|
CreatePhoneRegistrationTokenMutation,
|
||||||
|
CreatePhoneRegistrationTokenMutationVariables,
|
||||||
|
GetDevicePageQuery,
|
||||||
|
GetDevicePageQueryVariables,
|
||||||
|
GetActivePhoneMaintenanceCaseQuery,
|
||||||
|
GetActivePhoneMaintenanceCaseQueryVariables,
|
||||||
|
GetPhoneGpsTrackQuery,
|
||||||
|
GetPhoneGpsTrackQueryVariables,
|
||||||
|
GetPhoneMaintenanceHistoryQuery,
|
||||||
|
GetPhoneMaintenanceHistoryQueryVariables,
|
||||||
|
GetPhonePackagesQuery,
|
||||||
|
GetPhonePackagesQueryVariables,
|
||||||
|
GetPhoneQuery,
|
||||||
|
GetPhoneQueryVariables,
|
||||||
|
GetPhonesQuery,
|
||||||
|
GetPhonesQueryVariables,
|
||||||
|
GetPhonesTabsStatsQuery,
|
||||||
|
GetPhonesTabsStatsQueryVariables,
|
||||||
|
GetTelemetryQuery,
|
||||||
|
GetTelemetryQueryVariables,
|
||||||
|
MarkPhoneHealthyMutation,
|
||||||
|
MarkPhoneHealthyMutationVariables,
|
||||||
|
PhoneMaintenanceCaseStatus as GeneratedPhoneMaintenanceCaseStatus,
|
||||||
|
PhoneMalfunction as GeneratedPhoneMalfunction,
|
||||||
|
PhoneNetworkStatus,
|
||||||
|
PhoneTechnicalStatus as GeneratedPhoneTechnicalStatus,
|
||||||
|
ReturnPhoneFromMaintenanceMutation,
|
||||||
|
ReturnPhoneFromMaintenanceMutationVariables,
|
||||||
|
SendPhoneToMaintenanceMutation,
|
||||||
|
SendPhoneToMaintenanceMutationVariables,
|
||||||
|
ServiceUseOption as GeneratedServiceUseOption,
|
||||||
|
SetPhoneMaintenanceRequiredMutation,
|
||||||
|
SetPhoneMaintenanceRequiredMutationVariables,
|
||||||
|
} from '../../../shared/api/generated/graphql'
|
||||||
|
|
||||||
export type DeviceLocation = {
|
export type DeviceLocation = {
|
||||||
alt: number
|
alt: number
|
||||||
date: number
|
date: number
|
||||||
|
|
@ -5,14 +44,9 @@ export type DeviceLocation = {
|
||||||
lng: number
|
lng: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ServiceUseOption =
|
export type ServiceUseOption = GeneratedServiceUseOption
|
||||||
| 'Disabled'
|
|
||||||
| 'Enabled'
|
|
||||||
| 'Unspecified'
|
|
||||||
|
|
||||||
export type ComponentUseOption =
|
export type ComponentUseOption = GeneratedComponentUseOption
|
||||||
| 'Allowed'
|
|
||||||
| 'Disallowed'
|
|
||||||
|
|
||||||
export type PhoneUsePolicyOption = ServiceUseOption | ComponentUseOption
|
export type PhoneUsePolicyOption = ServiceUseOption | ComponentUseOption
|
||||||
|
|
||||||
|
|
@ -47,17 +81,23 @@ export type DevicePolicy = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DeviceTechState = {
|
export type DeviceTechState = {
|
||||||
batteryCycles: number | null
|
batteryCycles?: number | null
|
||||||
batteryLevel: number | null
|
batteryLevel?: number | null
|
||||||
batteryRemainingCapacity: number | null
|
batteryRemainingCapacity?: number | null
|
||||||
hits: number | null
|
hits?: number | null
|
||||||
needMaintenance: boolean
|
malfunctions?: DeviceMalfunction[]
|
||||||
overheats: number | null
|
malfunctionComment?: string | null
|
||||||
|
needMaintenance?: boolean
|
||||||
|
overheats?: number | null
|
||||||
|
status?: DeviceTechnicalStatus
|
||||||
temperature?: number | null
|
temperature?: number | null
|
||||||
worktime: number | null
|
worktime?: number | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DeviceNetworkStatus = 'Online' | 'Offline' | 'Lost'
|
export type DeviceNetworkStatus = PhoneNetworkStatus
|
||||||
|
export type DeviceTechnicalStatus = GeneratedPhoneTechnicalStatus
|
||||||
|
export type DeviceMalfunction = GeneratedPhoneMalfunction
|
||||||
|
export type DeviceMaintenanceCaseStatus = GeneratedPhoneMaintenanceCaseStatus
|
||||||
|
|
||||||
export type DeviceOrganisation = {
|
export type DeviceOrganisation = {
|
||||||
creationDate?: number
|
creationDate?: number
|
||||||
|
|
@ -70,60 +110,23 @@ export type DeviceOrganisation = {
|
||||||
export type Device = {
|
export type Device = {
|
||||||
id: number
|
id: number
|
||||||
imei: string
|
imei: string
|
||||||
imei2: string
|
imei2: string | null
|
||||||
serial: string
|
serial: string
|
||||||
networkStatus?: DeviceNetworkStatus
|
networkStatus: DeviceNetworkStatus
|
||||||
orgId: number
|
orgId: number
|
||||||
registerDate: number
|
registerDate?: number
|
||||||
org: DeviceOrganisation | null
|
org: DeviceOrganisation | null
|
||||||
policy: DevicePolicy | null
|
policy: DevicePolicy | null
|
||||||
techState: DeviceTechState | null
|
techState: DeviceTechState | null
|
||||||
lastLocation: DeviceLocation | null
|
lastLocation: DeviceLocation | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export type GetPhonesData = {
|
export type GetPhonesData = GetPhonesQuery
|
||||||
getPhones: {
|
export type GetPhonesVariables = GetPhonesQueryVariables
|
||||||
page: Device[]
|
export type GetPhonesTabsStatsData = GetPhonesTabsStatsQuery
|
||||||
totalElements: number
|
export type GetPhonesTabsStatsVariables = GetPhonesTabsStatsQueryVariables
|
||||||
totalPages: number
|
export type GetPhoneData = GetPhoneQuery
|
||||||
}
|
export type GetPhoneVariables = GetPhoneQueryVariables
|
||||||
}
|
|
||||||
|
|
||||||
export type GetPhonesVariables = {
|
|
||||||
page: number
|
|
||||||
query?: string
|
|
||||||
locked?: boolean
|
|
||||||
needMaintenance?: boolean
|
|
||||||
networkStatus?: DeviceNetworkStatus[]
|
|
||||||
orgs?: string[]
|
|
||||||
sortDirection: 'ASC' | 'DESC'
|
|
||||||
sortField: 'ID' | 'Serial' | 'Date'
|
|
||||||
}
|
|
||||||
|
|
||||||
type GetPhonesStatsPage = {
|
|
||||||
totalElements: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export type GetPhonesTabsStatsData = {
|
|
||||||
totalPhones: GetPhonesStatsPage
|
|
||||||
onlinePhones: GetPhonesStatsPage
|
|
||||||
lockedPhones: GetPhonesStatsPage
|
|
||||||
lostPhones: GetPhonesStatsPage
|
|
||||||
maintenancePhones: GetPhonesStatsPage
|
|
||||||
}
|
|
||||||
|
|
||||||
export type GetPhonesTabsStatsVariables = {
|
|
||||||
query?: string
|
|
||||||
orgs?: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export type GetPhoneData = {
|
|
||||||
getPhone: Device | null
|
|
||||||
}
|
|
||||||
|
|
||||||
export type GetPhoneVariables = {
|
|
||||||
id: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export type DeviceTelemetryItem = {
|
export type DeviceTelemetryItem = {
|
||||||
batteryCapacity: number | null
|
batteryCapacity: number | null
|
||||||
|
|
@ -174,13 +177,8 @@ export type DevicePackage = {
|
||||||
versionName: string | null
|
versionName: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export type GetPhonePackagesData = {
|
export type GetPhonePackagesData = GetPhonePackagesQuery
|
||||||
getPhonePackages: DevicePackage[]
|
export type GetPhonePackagesVariables = GetPhonePackagesQueryVariables
|
||||||
}
|
|
||||||
|
|
||||||
export type GetPhonePackagesVariables = {
|
|
||||||
phoneId: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export type DevicePackageUseEventType =
|
export type DevicePackageUseEventType =
|
||||||
| 'ResumeActivity'
|
| 'ResumeActivity'
|
||||||
|
|
@ -199,18 +197,8 @@ export type DevicePackageUseEventsGroup = {
|
||||||
phonePackage: DevicePackage | null
|
phonePackage: DevicePackage | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export type GetTelemetryData = {
|
export type GetTelemetryData = GetTelemetryQuery
|
||||||
getTelemetry: DeviceTelemetryItem[]
|
export type GetTelemetryVariables = GetTelemetryQueryVariables
|
||||||
getPhoneStateEvents: DevicePhoneStateEvent[]
|
|
||||||
getPhonePackagesUseEvents: DevicePackageUseEventsGroup[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export type GetTelemetryVariables = {
|
|
||||||
phoneId: number
|
|
||||||
packagesPhoneId: string
|
|
||||||
startDate: number
|
|
||||||
endDate?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export type DeviceGpsTrackPoint = {
|
export type DeviceGpsTrackPoint = {
|
||||||
alt: number
|
alt: number
|
||||||
|
|
@ -219,50 +207,33 @@ export type DeviceGpsTrackPoint = {
|
||||||
lng: number
|
lng: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export type GetPhoneGpsTrackData = {
|
export type GetPhoneGpsTrackData = GetPhoneGpsTrackQuery
|
||||||
getPhoneGpsTrack: DeviceGpsTrackPoint[]
|
export type GetPhoneGpsTrackVariables = GetPhoneGpsTrackQueryVariables
|
||||||
}
|
export type GetDevicePageData = GetDevicePageQuery
|
||||||
|
export type GetDevicePageVariables = GetDevicePageQueryVariables
|
||||||
export type GetPhoneGpsTrackVariables = {
|
export type ChangePhonePolicyData = ChangePhonePolicyMutation
|
||||||
phoneId: string
|
export type ChangePhonePolicyVariables = ChangePhonePolicyMutationVariables
|
||||||
startDate: number
|
export type CreatePhoneRegistrationTokenData =
|
||||||
endDate?: number
|
CreatePhoneRegistrationTokenMutation
|
||||||
}
|
export type CreatePhoneRegistrationTokenVariables =
|
||||||
|
CreatePhoneRegistrationTokenMutationVariables
|
||||||
export type GetDevicePageData = {
|
export type GetPhoneMaintenanceHistoryData = GetPhoneMaintenanceHistoryQuery
|
||||||
getPhone: Device | null
|
export type GetPhoneMaintenanceHistoryVariables =
|
||||||
getTelemetry: DeviceTelemetryItem[]
|
GetPhoneMaintenanceHistoryQueryVariables
|
||||||
getPhoneGpsTrack: DeviceGpsTrackPoint[]
|
export type GetActivePhoneMaintenanceCaseData =
|
||||||
}
|
GetActivePhoneMaintenanceCaseQuery
|
||||||
|
export type GetActivePhoneMaintenanceCaseVariables =
|
||||||
export type GetDevicePageVariables = {
|
GetActivePhoneMaintenanceCaseQueryVariables
|
||||||
id: number
|
export type SetPhoneMaintenanceRequiredData =
|
||||||
phoneId: string
|
SetPhoneMaintenanceRequiredMutation
|
||||||
telemetryStartDate: number
|
export type SetPhoneMaintenanceRequiredVariables =
|
||||||
telemetryEndDate?: number
|
SetPhoneMaintenanceRequiredMutationVariables
|
||||||
gpsStartDate: number
|
export type MarkPhoneHealthyData = MarkPhoneHealthyMutation
|
||||||
gpsEndDate?: number
|
export type MarkPhoneHealthyVariables = MarkPhoneHealthyMutationVariables
|
||||||
}
|
export type SendPhoneToMaintenanceData = SendPhoneToMaintenanceMutation
|
||||||
|
export type SendPhoneToMaintenanceVariables =
|
||||||
export type ChangePhonePolicyData = {
|
SendPhoneToMaintenanceMutationVariables
|
||||||
changePhonePolicy: DevicePolicy
|
export type ReturnPhoneFromMaintenanceData =
|
||||||
}
|
ReturnPhoneFromMaintenanceMutation
|
||||||
|
export type ReturnPhoneFromMaintenanceVariables =
|
||||||
export type ChangePhonePolicyVariables = {
|
ReturnPhoneFromMaintenanceMutationVariables
|
||||||
id: string
|
|
||||||
bluetooth: ServiceUseOption
|
|
||||||
camera: ComponentUseOption
|
|
||||||
GPS: ServiceUseOption
|
|
||||||
sim: ServiceUseOption
|
|
||||||
locked: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export type CreatePhoneRegistrationTokenData = {
|
|
||||||
createPhoneRegistrationToken: {
|
|
||||||
address: string
|
|
||||||
expiresIn: number
|
|
||||||
token: string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export type CreatePhoneRegistrationTokenVariables = Record<string, never>
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,299 @@
|
||||||
|
query GetUsers(
|
||||||
|
$page: Int!
|
||||||
|
$query: String
|
||||||
|
$orgs: [ID!]
|
||||||
|
$roles: [Role!]
|
||||||
|
$sortDirection: SortDirection!
|
||||||
|
$sortField: UserSortField!
|
||||||
|
) {
|
||||||
|
getUsers(
|
||||||
|
page: $page
|
||||||
|
query: $query
|
||||||
|
orgs: $orgs
|
||||||
|
roles: $roles
|
||||||
|
sortDirection: $sortDirection
|
||||||
|
sortField: $sortField
|
||||||
|
) {
|
||||||
|
totalPages
|
||||||
|
totalElements
|
||||||
|
page {
|
||||||
|
avatarUrl
|
||||||
|
firstName
|
||||||
|
id
|
||||||
|
lastName
|
||||||
|
middleName
|
||||||
|
org {
|
||||||
|
creationDate
|
||||||
|
id
|
||||||
|
logoUrl
|
||||||
|
name
|
||||||
|
}
|
||||||
|
orgId
|
||||||
|
role
|
||||||
|
username
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
query GetOrganisation($id: ID!) {
|
||||||
|
getOrganisation(id: $id) {
|
||||||
|
creationDate
|
||||||
|
id
|
||||||
|
name
|
||||||
|
logoUrl
|
||||||
|
policy {
|
||||||
|
bluetooth
|
||||||
|
bluetoothEditable
|
||||||
|
camera
|
||||||
|
cameraEditable
|
||||||
|
GPS
|
||||||
|
gpsEditable
|
||||||
|
sim
|
||||||
|
simEditable
|
||||||
|
offlineTimeThreshold
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mutation DeleteOrganisation($id: ID!) {
|
||||||
|
deleteOrganisation(id: $id)
|
||||||
|
}
|
||||||
|
|
||||||
|
query GetOrganisations(
|
||||||
|
$page: Int!
|
||||||
|
$query: String
|
||||||
|
$sortDirection: SortDirection!
|
||||||
|
$sortField: OrganisationSortField!
|
||||||
|
) {
|
||||||
|
getOrganisations(
|
||||||
|
page: $page
|
||||||
|
query: $query
|
||||||
|
sortDirection: $sortDirection
|
||||||
|
sortField: $sortField
|
||||||
|
) {
|
||||||
|
totalPages
|
||||||
|
totalElements
|
||||||
|
page {
|
||||||
|
creationDate
|
||||||
|
id
|
||||||
|
name
|
||||||
|
logoUrl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mutation CreateOrganisation(
|
||||||
|
$name: String!
|
||||||
|
$bluetooth: ServiceUseOption!
|
||||||
|
$bluetoothEditable: Boolean!
|
||||||
|
$camera: ComponentUseOption!
|
||||||
|
$cameraEditable: Boolean!
|
||||||
|
$GPS: ServiceUseOption!
|
||||||
|
$gpsEditable: Boolean!
|
||||||
|
$sim: ServiceUseOption!
|
||||||
|
$simEditable: Boolean!
|
||||||
|
$offlineTimeThreshold: Float!
|
||||||
|
) {
|
||||||
|
createOrganisation(
|
||||||
|
name: $name
|
||||||
|
policy: {
|
||||||
|
bluetooth: $bluetooth
|
||||||
|
bluetoothEditable: $bluetoothEditable
|
||||||
|
camera: $camera
|
||||||
|
cameraEditable: $cameraEditable
|
||||||
|
GPS: $GPS
|
||||||
|
gpsEditable: $gpsEditable
|
||||||
|
sim: $sim
|
||||||
|
simEditable: $simEditable
|
||||||
|
offlineTimeThreshold: $offlineTimeThreshold
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mutation ChangeOrganisation(
|
||||||
|
$id: ID!
|
||||||
|
$name: String!
|
||||||
|
$bluetooth: ServiceUseOption!
|
||||||
|
$bluetoothEditable: Boolean!
|
||||||
|
$camera: ComponentUseOption!
|
||||||
|
$cameraEditable: Boolean!
|
||||||
|
$GPS: ServiceUseOption!
|
||||||
|
$gpsEditable: Boolean!
|
||||||
|
$sim: ServiceUseOption!
|
||||||
|
$simEditable: Boolean!
|
||||||
|
$offlineTimeThreshold: Float!
|
||||||
|
) {
|
||||||
|
changeOrganisation(
|
||||||
|
id: $id
|
||||||
|
name: $name
|
||||||
|
policy: {
|
||||||
|
bluetooth: $bluetooth
|
||||||
|
bluetoothEditable: $bluetoothEditable
|
||||||
|
camera: $camera
|
||||||
|
cameraEditable: $cameraEditable
|
||||||
|
GPS: $GPS
|
||||||
|
gpsEditable: $gpsEditable
|
||||||
|
sim: $sim
|
||||||
|
simEditable: $simEditable
|
||||||
|
offlineTimeThreshold: $offlineTimeThreshold
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
creationDate
|
||||||
|
id
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mutation ChangeOrganisationPolicy(
|
||||||
|
$id: ID!
|
||||||
|
$name: String!
|
||||||
|
$bluetooth: ServiceUseOption!
|
||||||
|
$bluetoothEditable: Boolean!
|
||||||
|
$camera: ComponentUseOption!
|
||||||
|
$cameraEditable: Boolean!
|
||||||
|
$GPS: ServiceUseOption!
|
||||||
|
$gpsEditable: Boolean!
|
||||||
|
$sim: ServiceUseOption!
|
||||||
|
$simEditable: Boolean!
|
||||||
|
$offlineTimeThreshold: Float!
|
||||||
|
) {
|
||||||
|
changeOrganisation(
|
||||||
|
id: $id
|
||||||
|
name: $name
|
||||||
|
policy: {
|
||||||
|
bluetooth: $bluetooth
|
||||||
|
bluetoothEditable: $bluetoothEditable
|
||||||
|
camera: $camera
|
||||||
|
cameraEditable: $cameraEditable
|
||||||
|
GPS: $GPS
|
||||||
|
gpsEditable: $gpsEditable
|
||||||
|
sim: $sim
|
||||||
|
simEditable: $simEditable
|
||||||
|
offlineTimeThreshold: $offlineTimeThreshold
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
creationDate
|
||||||
|
id
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mutation createUser(
|
||||||
|
$orgId: ID!
|
||||||
|
$firstName: String!
|
||||||
|
$lastName: String!
|
||||||
|
$middleName: String!
|
||||||
|
$username: String!
|
||||||
|
$password: String!
|
||||||
|
$role: Role!
|
||||||
|
) {
|
||||||
|
createUser(
|
||||||
|
payload: {
|
||||||
|
orgId: $orgId
|
||||||
|
firstName: $firstName
|
||||||
|
lastName: $lastName
|
||||||
|
middleName: $middleName
|
||||||
|
username: $username
|
||||||
|
password: $password
|
||||||
|
role: $role
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
id
|
||||||
|
firstName
|
||||||
|
lastName
|
||||||
|
middleName
|
||||||
|
orgId
|
||||||
|
role
|
||||||
|
org {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mutation ChangeUser(
|
||||||
|
$userId: ID!
|
||||||
|
$orgId: ID!
|
||||||
|
$firstName: String!
|
||||||
|
$lastName: String!
|
||||||
|
$middleName: String!
|
||||||
|
$username: String!
|
||||||
|
$role: Role!
|
||||||
|
) {
|
||||||
|
changeUser(
|
||||||
|
userId: $userId
|
||||||
|
form: {
|
||||||
|
orgId: $orgId
|
||||||
|
firstName: $firstName
|
||||||
|
lastName: $lastName
|
||||||
|
middleName: $middleName
|
||||||
|
username: $username
|
||||||
|
role: $role
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
avatarUrl
|
||||||
|
firstName
|
||||||
|
id
|
||||||
|
lastName
|
||||||
|
middleName
|
||||||
|
org {
|
||||||
|
creationDate
|
||||||
|
id
|
||||||
|
logoUrl
|
||||||
|
name
|
||||||
|
}
|
||||||
|
orgId
|
||||||
|
role
|
||||||
|
username
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mutation ChangeUserWithPassword(
|
||||||
|
$userId: ID!
|
||||||
|
$orgId: ID!
|
||||||
|
$firstName: String!
|
||||||
|
$lastName: String!
|
||||||
|
$middleName: String!
|
||||||
|
$username: String!
|
||||||
|
$password: String!
|
||||||
|
$role: Role!
|
||||||
|
) {
|
||||||
|
changeUser(
|
||||||
|
userId: $userId
|
||||||
|
form: {
|
||||||
|
orgId: $orgId
|
||||||
|
firstName: $firstName
|
||||||
|
lastName: $lastName
|
||||||
|
middleName: $middleName
|
||||||
|
username: $username
|
||||||
|
password: $password
|
||||||
|
role: $role
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
avatarUrl
|
||||||
|
firstName
|
||||||
|
id
|
||||||
|
lastName
|
||||||
|
middleName
|
||||||
|
org {
|
||||||
|
creationDate
|
||||||
|
id
|
||||||
|
logoUrl
|
||||||
|
name
|
||||||
|
}
|
||||||
|
orgId
|
||||||
|
role
|
||||||
|
username
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mutation CreateUploadUserAvatarUrl($userId: ID!) {
|
||||||
|
createUploadUserAvatarUrl(userId: $userId)
|
||||||
|
}
|
||||||
|
|
||||||
|
mutation CreateUploadOrganisationLogoUrl($orgId: ID!) {
|
||||||
|
createUploadOrganisationLogoUrl(orgId: $orgId)
|
||||||
|
}
|
||||||
|
|
@ -1,325 +1,14 @@
|
||||||
import { gql } from '@apollo/client'
|
export {
|
||||||
|
GetUsersDocument as GET_USERS_QUERY,
|
||||||
export const GET_USERS_QUERY = gql`
|
GetOrganisationDocument as GET_ORGANISATION_QUERY,
|
||||||
query GetUsers(
|
DeleteOrganisationDocument as DELETE_ORGANISATION_MUTATION,
|
||||||
$page: Int!
|
GetOrganisationsDocument as GET_ORGANISATIONS_QUERY,
|
||||||
$query: String
|
CreateOrganisationDocument as CREATE_ORGANISATION_MUTATION,
|
||||||
$orgs: [ID!]
|
ChangeOrganisationDocument as CHANGE_ORGANISATION_MUTATION,
|
||||||
$roles: [Role!]
|
ChangeOrganisationPolicyDocument as CHANGE_ORGANISATION_POLICY_MUTATION,
|
||||||
$sortDirection: SortDirection!
|
CreateUserDocument as CREATE_USER_MUTATION,
|
||||||
$sortField: UserSortField!
|
ChangeUserDocument as CHANGE_USER_MUTATION,
|
||||||
) {
|
ChangeUserWithPasswordDocument as CHANGE_USER_WITH_PASSWORD_MUTATION,
|
||||||
getUsers(
|
CreateUploadUserAvatarUrlDocument as CREATE_UPLOAD_USER_AVATAR_URL_MUTATION,
|
||||||
page: $page
|
CreateUploadOrganisationLogoUrlDocument as CREATE_UPLOAD_ORGANISATION_LOGO_URL_MUTATION,
|
||||||
query: $query
|
} from '../../../shared/api/generated/graphql'
|
||||||
orgs: $orgs
|
|
||||||
roles: $roles
|
|
||||||
sortDirection: $sortDirection
|
|
||||||
sortField: $sortField
|
|
||||||
) {
|
|
||||||
totalPages
|
|
||||||
totalElements
|
|
||||||
page {
|
|
||||||
avatarUrl
|
|
||||||
firstName
|
|
||||||
id
|
|
||||||
lastName
|
|
||||||
middleName
|
|
||||||
org {
|
|
||||||
creationDate
|
|
||||||
id
|
|
||||||
logoUrl
|
|
||||||
name
|
|
||||||
}
|
|
||||||
orgId
|
|
||||||
role
|
|
||||||
username
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
export const GET_ORGANISATION_QUERY = gql`
|
|
||||||
query GetOrganisation($id: ID!) {
|
|
||||||
getOrganisation(id: $id) {
|
|
||||||
creationDate
|
|
||||||
id
|
|
||||||
name
|
|
||||||
logoUrl
|
|
||||||
policy {
|
|
||||||
bluetooth
|
|
||||||
bluetoothEditable
|
|
||||||
camera
|
|
||||||
cameraEditable
|
|
||||||
GPS
|
|
||||||
gpsEditable
|
|
||||||
sim
|
|
||||||
simEditable
|
|
||||||
offlineTimeThreshold
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
export const DELETE_ORGANISATION_MUTATION = gql`
|
|
||||||
mutation DeleteOrganisation($id: ID!) {
|
|
||||||
deleteOrganisation(id: $id)
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
export const GET_ORGANISATIONS_QUERY = gql`
|
|
||||||
query GetOrganisations(
|
|
||||||
$page: Int!
|
|
||||||
$query: String
|
|
||||||
$sortDirection: SortDirection!
|
|
||||||
$sortField: OrganisationSortField!
|
|
||||||
) {
|
|
||||||
getOrganisations(
|
|
||||||
page: $page
|
|
||||||
query: $query
|
|
||||||
sortDirection: $sortDirection
|
|
||||||
sortField: $sortField
|
|
||||||
) {
|
|
||||||
totalPages
|
|
||||||
totalElements
|
|
||||||
page {
|
|
||||||
creationDate
|
|
||||||
id
|
|
||||||
name
|
|
||||||
logoUrl
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
export const CREATE_ORGANISATION_MUTATION = gql`
|
|
||||||
mutation CreateOrganisation(
|
|
||||||
$name: String!
|
|
||||||
$bluetooth: ServiceUseOption!
|
|
||||||
$bluetoothEditable: Boolean!
|
|
||||||
$camera: ComponentUseOption!
|
|
||||||
$cameraEditable: Boolean!
|
|
||||||
$GPS: ServiceUseOption!
|
|
||||||
$gpsEditable: Boolean!
|
|
||||||
$sim: ServiceUseOption!
|
|
||||||
$simEditable: Boolean!
|
|
||||||
$offlineTimeThreshold: Float!
|
|
||||||
) {
|
|
||||||
createOrganisation(
|
|
||||||
name: $name
|
|
||||||
policy: {
|
|
||||||
bluetooth: $bluetooth
|
|
||||||
bluetoothEditable: $bluetoothEditable
|
|
||||||
camera: $camera
|
|
||||||
cameraEditable: $cameraEditable
|
|
||||||
GPS: $GPS
|
|
||||||
gpsEditable: $gpsEditable
|
|
||||||
sim: $sim
|
|
||||||
simEditable: $simEditable
|
|
||||||
offlineTimeThreshold: $offlineTimeThreshold
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
export const CHANGE_ORGANISATION_MUTATION = gql`
|
|
||||||
mutation ChangeOrganisation(
|
|
||||||
$id: ID!
|
|
||||||
$name: String!
|
|
||||||
$bluetooth: ServiceUseOption!
|
|
||||||
$bluetoothEditable: Boolean!
|
|
||||||
$camera: ComponentUseOption!
|
|
||||||
$cameraEditable: Boolean!
|
|
||||||
$GPS: ServiceUseOption!
|
|
||||||
$gpsEditable: Boolean!
|
|
||||||
$sim: ServiceUseOption!
|
|
||||||
$simEditable: Boolean!
|
|
||||||
$offlineTimeThreshold: Float!
|
|
||||||
) {
|
|
||||||
changeOrganisation(
|
|
||||||
id: $id
|
|
||||||
name: $name
|
|
||||||
policy: {
|
|
||||||
bluetooth: $bluetooth
|
|
||||||
bluetoothEditable: $bluetoothEditable
|
|
||||||
camera: $camera
|
|
||||||
cameraEditable: $cameraEditable
|
|
||||||
GPS: $GPS
|
|
||||||
gpsEditable: $gpsEditable
|
|
||||||
sim: $sim
|
|
||||||
simEditable: $simEditable
|
|
||||||
offlineTimeThreshold: $offlineTimeThreshold
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
creationDate
|
|
||||||
id
|
|
||||||
name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
export const CHANGE_ORGANISATION_POLICY_MUTATION = gql`
|
|
||||||
mutation ChangeOrganisationPolicy(
|
|
||||||
$id: ID!
|
|
||||||
$name: String!
|
|
||||||
$bluetooth: ServiceUseOption!
|
|
||||||
$bluetoothEditable: Boolean!
|
|
||||||
$camera: ComponentUseOption!
|
|
||||||
$cameraEditable: Boolean!
|
|
||||||
$GPS: ServiceUseOption!
|
|
||||||
$gpsEditable: Boolean!
|
|
||||||
$sim: ServiceUseOption!
|
|
||||||
$simEditable: Boolean!
|
|
||||||
$offlineTimeThreshold: Float!
|
|
||||||
) {
|
|
||||||
changeOrganisation(
|
|
||||||
id: $id
|
|
||||||
name: $name
|
|
||||||
policy: {
|
|
||||||
bluetooth: $bluetooth
|
|
||||||
bluetoothEditable: $bluetoothEditable
|
|
||||||
camera: $camera
|
|
||||||
cameraEditable: $cameraEditable
|
|
||||||
GPS: $GPS
|
|
||||||
gpsEditable: $gpsEditable
|
|
||||||
sim: $sim
|
|
||||||
simEditable: $simEditable
|
|
||||||
offlineTimeThreshold: $offlineTimeThreshold
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
creationDate
|
|
||||||
id
|
|
||||||
name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
export const CREATE_USER_MUTATION = gql`
|
|
||||||
mutation createUser(
|
|
||||||
$orgId: ID!
|
|
||||||
$firstName: String!
|
|
||||||
$lastName: String!
|
|
||||||
$middleName: String!
|
|
||||||
$username: String!
|
|
||||||
$password: String!
|
|
||||||
$role: Role!
|
|
||||||
) {
|
|
||||||
createUser(
|
|
||||||
payload: {
|
|
||||||
orgId: $orgId
|
|
||||||
firstName: $firstName
|
|
||||||
lastName: $lastName
|
|
||||||
middleName: $middleName
|
|
||||||
username: $username
|
|
||||||
password: $password
|
|
||||||
role: $role
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
id
|
|
||||||
firstName
|
|
||||||
lastName
|
|
||||||
middleName
|
|
||||||
orgId
|
|
||||||
role
|
|
||||||
org {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
export const CHANGE_USER_MUTATION = gql`
|
|
||||||
mutation ChangeUser(
|
|
||||||
$userId: ID!
|
|
||||||
$orgId: ID!
|
|
||||||
$firstName: String!
|
|
||||||
$lastName: String!
|
|
||||||
$middleName: String!
|
|
||||||
$username: String!
|
|
||||||
$role: Role!
|
|
||||||
) {
|
|
||||||
changeUser(
|
|
||||||
userId: $userId
|
|
||||||
form: {
|
|
||||||
orgId: $orgId
|
|
||||||
firstName: $firstName
|
|
||||||
lastName: $lastName
|
|
||||||
middleName: $middleName
|
|
||||||
username: $username
|
|
||||||
role: $role
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
avatarUrl
|
|
||||||
firstName
|
|
||||||
id
|
|
||||||
lastName
|
|
||||||
middleName
|
|
||||||
org {
|
|
||||||
creationDate
|
|
||||||
id
|
|
||||||
logoUrl
|
|
||||||
name
|
|
||||||
}
|
|
||||||
orgId
|
|
||||||
role
|
|
||||||
username
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
export const CHANGE_USER_WITH_PASSWORD_MUTATION = gql`
|
|
||||||
mutation ChangeUserWithPassword(
|
|
||||||
$userId: ID!
|
|
||||||
$orgId: ID!
|
|
||||||
$firstName: String!
|
|
||||||
$lastName: String!
|
|
||||||
$middleName: String!
|
|
||||||
$username: String!
|
|
||||||
$password: String!
|
|
||||||
$role: Role!
|
|
||||||
) {
|
|
||||||
changeUser(
|
|
||||||
userId: $userId
|
|
||||||
form: {
|
|
||||||
orgId: $orgId
|
|
||||||
firstName: $firstName
|
|
||||||
lastName: $lastName
|
|
||||||
middleName: $middleName
|
|
||||||
username: $username
|
|
||||||
password: $password
|
|
||||||
role: $role
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
avatarUrl
|
|
||||||
firstName
|
|
||||||
id
|
|
||||||
lastName
|
|
||||||
middleName
|
|
||||||
org {
|
|
||||||
creationDate
|
|
||||||
id
|
|
||||||
logoUrl
|
|
||||||
name
|
|
||||||
}
|
|
||||||
orgId
|
|
||||||
role
|
|
||||||
username
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
export const CREATE_UPLOAD_USER_AVATAR_URL_MUTATION = gql`
|
|
||||||
mutation CreateUploadUserAvatarUrl($userId: ID!) {
|
|
||||||
createUploadUserAvatarUrl(userId: $userId)
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
export const CREATE_UPLOAD_ORGANISATION_LOGO_URL_MUTATION = gql`
|
|
||||||
mutation CreateUploadOrganisationLogoUrl($orgId: ID!) {
|
|
||||||
createUploadOrganisationLogoUrl(orgId: $orgId)
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,42 @@
|
||||||
export type EmployeeRole = 'User' | 'Admin' | string
|
import type {
|
||||||
|
ChangeOrganisationMutation,
|
||||||
|
ChangeOrganisationMutationVariables,
|
||||||
|
ChangeOrganisationPolicyMutationVariables,
|
||||||
|
ChangeUserMutation,
|
||||||
|
ChangeUserMutationVariables,
|
||||||
|
ChangeUserWithPasswordMutationVariables,
|
||||||
|
ComponentUseOption as GeneratedComponentUseOption,
|
||||||
|
CreateOrganisationMutation,
|
||||||
|
CreateOrganisationMutationVariables,
|
||||||
|
CreateUploadOrganisationLogoUrlMutation,
|
||||||
|
CreateUploadOrganisationLogoUrlMutationVariables,
|
||||||
|
CreateUploadUserAvatarUrlMutation,
|
||||||
|
CreateUploadUserAvatarUrlMutationVariables,
|
||||||
|
CreateUserMutation,
|
||||||
|
CreateUserMutationVariables,
|
||||||
|
DeleteOrganisationMutation,
|
||||||
|
DeleteOrganisationMutationVariables,
|
||||||
|
GetOrganisationQuery,
|
||||||
|
GetOrganisationQueryVariables,
|
||||||
|
GetOrganisationsQuery,
|
||||||
|
GetOrganisationsQueryVariables,
|
||||||
|
GetUsersQuery,
|
||||||
|
GetUsersQueryVariables,
|
||||||
|
OrganisationSortField as GeneratedOrganisationSortField,
|
||||||
|
Role,
|
||||||
|
ServiceUseOption as GeneratedServiceUseOption,
|
||||||
|
SortDirection,
|
||||||
|
UserSortField as GeneratedUserSortField,
|
||||||
|
} from '../../../shared/api/generated/graphql'
|
||||||
|
|
||||||
|
export type EmployeeRole = Role
|
||||||
|
|
||||||
export type Employee = {
|
export type Employee = {
|
||||||
avatarUrl?: string | null
|
avatarUrl?: string | null
|
||||||
id: number
|
id: number
|
||||||
firstName: string
|
firstName: string
|
||||||
lastName: string
|
lastName: string
|
||||||
middleName: string
|
middleName: string | null
|
||||||
username?: string
|
username?: string
|
||||||
orgId: number
|
orgId: number
|
||||||
role: EmployeeRole
|
role: EmployeeRole
|
||||||
|
|
@ -17,41 +48,19 @@ export type Employee = {
|
||||||
} | null
|
} | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserSortDirection = 'ASC' | 'DESC'
|
export type UserSortDirection = SortDirection
|
||||||
|
|
||||||
export type UserSortField = 'ID' | 'Name' | 'Date'
|
export type UserSortField = GeneratedUserSortField
|
||||||
|
|
||||||
export type ServiceUseOption =
|
export type ServiceUseOption = GeneratedServiceUseOption
|
||||||
| 'Disabled'
|
|
||||||
| 'Enabled'
|
|
||||||
| 'Unspecified'
|
|
||||||
|
|
||||||
export type ComponentUseOption =
|
export type ComponentUseOption = GeneratedComponentUseOption
|
||||||
| 'Allowed'
|
|
||||||
| 'Disallowed'
|
|
||||||
|
|
||||||
export type GroupUsePolicyOption =
|
export type GroupUsePolicyOption =
|
||||||
| 'Allowed'
|
| 'Allowed'
|
||||||
| 'Disallowed'
|
| 'Disallowed'
|
||||||
| 'Changeable'
|
| 'Changeable'
|
||||||
|
|
||||||
export type GetUsersData = {
|
|
||||||
getUsers: {
|
|
||||||
totalPages: number
|
|
||||||
totalElements: number
|
|
||||||
page: Employee[]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export type GetUsersVariables = {
|
|
||||||
page: number
|
|
||||||
query?: string
|
|
||||||
orgs?: string[]
|
|
||||||
roles?: EmployeeRole[]
|
|
||||||
sortDirection: UserSortDirection
|
|
||||||
sortField: UserSortField
|
|
||||||
}
|
|
||||||
|
|
||||||
export type OrganisationPolicy = {
|
export type OrganisationPolicy = {
|
||||||
bluetooth?: ServiceUseOption | GroupUsePolicyOption
|
bluetooth?: ServiceUseOption | GroupUsePolicyOption
|
||||||
bluetoothEditable?: boolean
|
bluetoothEditable?: boolean
|
||||||
|
|
@ -77,161 +86,33 @@ export type Organisation = {
|
||||||
logoUrl?: string | null
|
logoUrl?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export type GetOrganisationData = {
|
export type GetUsersData = GetUsersQuery
|
||||||
getOrganisation: Organisation | null
|
export type GetUsersVariables = GetUsersQueryVariables
|
||||||
}
|
export type GetOrganisationData = GetOrganisationQuery
|
||||||
|
export type GetOrganisationVariables = GetOrganisationQueryVariables
|
||||||
export type GetOrganisationVariables = {
|
export type ChangeOrganisationData = ChangeOrganisationMutation
|
||||||
id: string
|
export type ChangeOrganisationVariables = ChangeOrganisationMutationVariables
|
||||||
}
|
export type ChangeOrganisationPolicyVariables =
|
||||||
|
ChangeOrganisationPolicyMutationVariables
|
||||||
export type ChangeOrganisationData = {
|
export type GetOrganisationsData = GetOrganisationsQuery
|
||||||
changeOrganisation: {
|
export type GetOrganisationsVariables = GetOrganisationsQueryVariables
|
||||||
id: number
|
export type DeleteOrganisationData = DeleteOrganisationMutation
|
||||||
name: string
|
export type DeleteOrganisationVariables = DeleteOrganisationMutationVariables
|
||||||
}
|
export type CreateUserData = CreateUserMutation
|
||||||
}
|
export type CreateUserVariables = CreateUserMutationVariables
|
||||||
|
export type ChangeUserData = ChangeUserMutation
|
||||||
export type ChangeOrganisationVariables = {
|
export type ChangeUserVariables = ChangeUserMutationVariables
|
||||||
id: string
|
export type ChangeUserWithPasswordVariables =
|
||||||
name: string
|
ChangeUserWithPasswordMutationVariables
|
||||||
bluetooth: ServiceUseOption
|
export type CreateOrganisationData = CreateOrganisationMutation
|
||||||
bluetoothEditable: boolean
|
export type CreateOrganisationVariables = CreateOrganisationMutationVariables
|
||||||
camera: ComponentUseOption
|
export type OrganisationSortDirection = SortDirection
|
||||||
cameraEditable: boolean
|
export type OrganisationSortField = GeneratedOrganisationSortField
|
||||||
GPS: ServiceUseOption
|
export type CreateUploadUserAvatarUrlData =
|
||||||
gpsEditable: boolean
|
CreateUploadUserAvatarUrlMutation
|
||||||
sim: ServiceUseOption
|
export type CreateUploadUserAvatarUrlVariables =
|
||||||
simEditable: boolean
|
CreateUploadUserAvatarUrlMutationVariables
|
||||||
offlineTimeThreshold: number
|
export type CreateUploadOrganisationLogoUrlData =
|
||||||
}
|
CreateUploadOrganisationLogoUrlMutation
|
||||||
|
export type CreateUploadOrganisationLogoUrlVariables =
|
||||||
export type ChangeOrganisationPolicyVariables = {
|
CreateUploadOrganisationLogoUrlMutationVariables
|
||||||
id: string
|
|
||||||
name: string
|
|
||||||
bluetooth: ServiceUseOption
|
|
||||||
bluetoothEditable: boolean
|
|
||||||
camera: ComponentUseOption
|
|
||||||
cameraEditable: boolean
|
|
||||||
GPS: ServiceUseOption
|
|
||||||
gpsEditable: boolean
|
|
||||||
sim: ServiceUseOption
|
|
||||||
simEditable: boolean
|
|
||||||
offlineTimeThreshold: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export type GetUsersPageData = {
|
|
||||||
getUsersPage: {
|
|
||||||
page: Employee[]
|
|
||||||
nextKey: string | null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export type GetUsersPageVariables = {
|
|
||||||
key?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export type GetOrganisationsData = {
|
|
||||||
getOrganisations: {
|
|
||||||
totalPages: number
|
|
||||||
totalElements: number
|
|
||||||
page: Organisation[]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export type GetOrganisationsVariables = {
|
|
||||||
page: number
|
|
||||||
query?: string
|
|
||||||
sortDirection: OrganisationSortDirection
|
|
||||||
sortField: OrganisationSortField
|
|
||||||
}
|
|
||||||
|
|
||||||
export type DeleteOrganisationData = {
|
|
||||||
deleteOrganisation: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export type DeleteOrganisationVariables = {
|
|
||||||
id: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export type CreateUserData = {
|
|
||||||
createUser: Employee
|
|
||||||
}
|
|
||||||
|
|
||||||
export type CreateUserVariables = {
|
|
||||||
orgId: string
|
|
||||||
firstName: string
|
|
||||||
lastName: string
|
|
||||||
middleName: string
|
|
||||||
username: string
|
|
||||||
password: string
|
|
||||||
role: EmployeeRole
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ChangeUserData = {
|
|
||||||
changeUser: Employee
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ChangeUserVariables = {
|
|
||||||
userId: string
|
|
||||||
orgId: string
|
|
||||||
firstName: string
|
|
||||||
lastName: string
|
|
||||||
middleName: string
|
|
||||||
username: string
|
|
||||||
role: EmployeeRole
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ChangeUserWithPasswordVariables = ChangeUserVariables & {
|
|
||||||
password: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export type SignUpVariables = {
|
|
||||||
orgId: string
|
|
||||||
firstName: string
|
|
||||||
lastName: string
|
|
||||||
middleName: string
|
|
||||||
username: string
|
|
||||||
password: string
|
|
||||||
role: EmployeeRole
|
|
||||||
}
|
|
||||||
|
|
||||||
export type CreateOrganisationData = {
|
|
||||||
createOrganisation: {
|
|
||||||
id: number
|
|
||||||
name: string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export type CreateOrganisationVariables = {
|
|
||||||
name: string
|
|
||||||
bluetooth: ServiceUseOption
|
|
||||||
bluetoothEditable: boolean
|
|
||||||
camera: ComponentUseOption
|
|
||||||
cameraEditable: boolean
|
|
||||||
GPS: ServiceUseOption
|
|
||||||
gpsEditable: boolean
|
|
||||||
sim: ServiceUseOption
|
|
||||||
simEditable: boolean
|
|
||||||
offlineTimeThreshold: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export type OrganisationSortDirection = 'ASC' | 'DESC'
|
|
||||||
|
|
||||||
export type OrganisationSortField = 'ID' | 'Name' | 'Date'
|
|
||||||
|
|
||||||
export type CreateUploadUserAvatarUrlData = {
|
|
||||||
createUploadUserAvatarUrl: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export type CreateUploadUserAvatarUrlVariables = {
|
|
||||||
userId: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export type CreateUploadOrganisationLogoUrlData = {
|
|
||||||
createUploadOrganisationLogoUrl: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export type CreateUploadOrganisationLogoUrlVariables = {
|
|
||||||
orgId: string
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
mutation SignIn($username: String!, $password: String!) {
|
||||||
|
signIn(username: $username, password: $password) {
|
||||||
|
id
|
||||||
|
role
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mutation RefreshSession {
|
||||||
|
refreshSession {
|
||||||
|
id
|
||||||
|
firstName
|
||||||
|
lastName
|
||||||
|
middleName
|
||||||
|
orgId
|
||||||
|
role
|
||||||
|
org {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
query CurrentUser {
|
||||||
|
currentUser {
|
||||||
|
avatarUrl
|
||||||
|
id
|
||||||
|
role
|
||||||
|
firstName
|
||||||
|
middleName
|
||||||
|
lastName
|
||||||
|
username
|
||||||
|
org {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,45 +1,5 @@
|
||||||
import { gql } from '@apollo/client'
|
export {
|
||||||
|
SignInDocument as SIGN_IN_MUTATION,
|
||||||
export const SIGN_IN_MUTATION = gql`
|
RefreshSessionDocument as REFRESH_SESSION_MUTATION,
|
||||||
mutation SignIn($username: String!, $password: String!) {
|
CurrentUserDocument as CURRENT_USER_QUERY,
|
||||||
signIn(username: $username, password: $password) {
|
} from '../../../shared/api/generated/graphql'
|
||||||
id
|
|
||||||
role
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
export const REFRESH_SESSION_MUTATION = gql`
|
|
||||||
mutation RefreshSession {
|
|
||||||
refreshSession {
|
|
||||||
id
|
|
||||||
firstName
|
|
||||||
lastName
|
|
||||||
middleName
|
|
||||||
orgId
|
|
||||||
role
|
|
||||||
org {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
export const CURRENT_USER_QUERY = gql`
|
|
||||||
query CurrentUser {
|
|
||||||
currentUser {
|
|
||||||
avatarUrl
|
|
||||||
id
|
|
||||||
role
|
|
||||||
firstName
|
|
||||||
middleName
|
|
||||||
lastName
|
|
||||||
username
|
|
||||||
org {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
|
||||||
|
|
@ -8,28 +8,6 @@ import {
|
||||||
} from '../api/auth.graphql'
|
} from '../api/auth.graphql'
|
||||||
import { LoginPage } from '../../../pages/LoginPage/LoginPage'
|
import { LoginPage } from '../../../pages/LoginPage/LoginPage'
|
||||||
|
|
||||||
type CurrentUser = {
|
|
||||||
id: string
|
|
||||||
role: string
|
|
||||||
avatarUrl: string
|
|
||||||
firstName: string
|
|
||||||
middleName: string
|
|
||||||
lastName: string
|
|
||||||
username: string
|
|
||||||
org: {
|
|
||||||
id: number
|
|
||||||
name: string
|
|
||||||
} | null
|
|
||||||
}
|
|
||||||
|
|
||||||
type CurrentUserQueryData = {
|
|
||||||
currentUser: CurrentUser | null
|
|
||||||
}
|
|
||||||
|
|
||||||
type RefreshSessionData = {
|
|
||||||
refreshSession: CurrentUser | null
|
|
||||||
}
|
|
||||||
|
|
||||||
type AuthGateProps = {
|
type AuthGateProps = {
|
||||||
children: ReactNode
|
children: ReactNode
|
||||||
}
|
}
|
||||||
|
|
@ -38,7 +16,7 @@ export function AuthGate({ children }: AuthGateProps) {
|
||||||
const [isForcedLogout, setIsForcedLogout] = useState(false)
|
const [isForcedLogout, setIsForcedLogout] = useState(false)
|
||||||
const [isRefreshFailed, setIsRefreshFailed] = useState(false)
|
const [isRefreshFailed, setIsRefreshFailed] = useState(false)
|
||||||
|
|
||||||
const { data, loading, error, refetch } = useQuery<CurrentUserQueryData>(
|
const { data, loading, error, refetch } = useQuery(
|
||||||
CURRENT_USER_QUERY,
|
CURRENT_USER_QUERY,
|
||||||
{
|
{
|
||||||
fetchPolicy: 'network-only',
|
fetchPolicy: 'network-only',
|
||||||
|
|
@ -47,7 +25,7 @@ export function AuthGate({ children }: AuthGateProps) {
|
||||||
)
|
)
|
||||||
|
|
||||||
const [refreshSession, { loading: isRefreshing }] =
|
const [refreshSession, { loading: isRefreshing }] =
|
||||||
useMutation<RefreshSessionData>(REFRESH_SESSION_MUTATION, {
|
useMutation(REFRESH_SESSION_MUTATION, {
|
||||||
onCompleted: async (result) => {
|
onCompleted: async (result) => {
|
||||||
if (!result.refreshSession) {
|
if (!result.refreshSession) {
|
||||||
setIsRefreshFailed(true)
|
setIsRefreshFailed(true)
|
||||||
|
|
@ -112,4 +90,4 @@ export function AuthGate({ children }: AuthGateProps) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return children
|
return children
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,18 @@ html{
|
||||||
background: $color-bg;
|
background: $color-bg;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
::view-transition-old(root),
|
||||||
|
::view-transition-new(root) {
|
||||||
|
animation: none;
|
||||||
|
mix-blend-mode: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.theme-transition-no-css-transitions *,
|
||||||
|
html.theme-transition-no-css-transitions *::before,
|
||||||
|
html.theme-transition-no-css-transitions *::after {
|
||||||
|
transition: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
button{
|
button{
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,35 @@
|
||||||
padding: 20px 20px 36px 20px;
|
padding: 20px 20px 36px 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.device-page--apps {
|
||||||
|
flex: 1 1 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 0;
|
||||||
|
height: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
overflow: visible;
|
||||||
|
|
||||||
|
.device-page__motion-view {
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-page__apps-motion {
|
||||||
|
display: flex;
|
||||||
|
flex: 1 1 0;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 0;
|
||||||
|
height: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-apps-view {
|
||||||
|
min-height: 0;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.device-page__grid {
|
.device-page__grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 340px 340px minmax(360px, 1fr);
|
grid-template-columns: 340px 340px minmax(360px, 1fr);
|
||||||
|
|
@ -30,7 +59,7 @@
|
||||||
|
|
||||||
.device-card {
|
.device-card {
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
background: rgba($color-surface-rgb, .5);
|
background: $color-surface;
|
||||||
backdrop-filter: blur(22px);
|
backdrop-filter: blur(22px);
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
box-shadow: $shadow-card;
|
box-shadow: $shadow-card;
|
||||||
|
|
@ -116,35 +145,153 @@
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.device-status-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
.device-status {
|
.device-status {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 9px;
|
gap: 2px;
|
||||||
|
|
||||||
color: $color-text-strong;
|
color: $color-text-strong;
|
||||||
font-size: $font-size-18;
|
font-size: $font-size-14;
|
||||||
font-weight: 500;
|
font-weight: 550;
|
||||||
|
text-transform: uppercase;
|
||||||
|
|
||||||
|
padding-right: 12px;
|
||||||
|
border-radius: 12px;
|
||||||
|
line-height: 1;
|
||||||
|
border: none;
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
padding: 4px;
|
padding: 6px;
|
||||||
width: 24px;
|
width: 20px;
|
||||||
height: 24px;
|
height: 20px;
|
||||||
border-radius: 8px;
|
border-radius: 12px;
|
||||||
background: $color-bg;
|
background: $color-bg;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.is-success svg {
|
&.is-success {
|
||||||
color: $green;
|
color: $green;
|
||||||
background: $color-success-bg;
|
background-color: var(--color-success-bg);
|
||||||
|
|
||||||
|
svg {
|
||||||
|
color: $green;
|
||||||
|
background: $color-success-bg;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.is-danger svg {
|
&.is-danger {
|
||||||
color: $red;
|
|
||||||
background: $color-danger-bg;
|
background: $color-danger-bg;
|
||||||
|
color: $red;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
color: $red;
|
||||||
|
background: $color-danger-bg;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.is-muted svg {
|
&.is-muted {
|
||||||
|
background: var(--color-bg);
|
||||||
color: $gray50;
|
color: $gray50;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
color: $gray50;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-service {
|
||||||
|
background: $color-primary-muted;
|
||||||
|
color: $blue;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
color: $blue;
|
||||||
|
background: $color-primary-muted;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-status--interactive {
|
||||||
|
border: 0;
|
||||||
|
font-family: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
transition:
|
||||||
|
filter 0.18s ease,
|
||||||
|
box-shadow 0.18s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-status-menu__trigger {
|
||||||
|
width: auto;
|
||||||
|
aspect-ratio: 1/1;
|
||||||
|
height: 100%;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: transparent;
|
||||||
|
padding: 0;
|
||||||
|
color: $gray50;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: 0.18s ease;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&[data-state='open'] {
|
||||||
|
background: $color-bg;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
outline-offset: 1px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-status-menu {
|
||||||
|
z-index: 600;
|
||||||
|
min-width: 228px;
|
||||||
|
border: 1px solid $color-border-muted;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: $color-surface;
|
||||||
|
padding: 6px;
|
||||||
|
box-shadow: $shadow-popover;
|
||||||
|
animation: device-status-menu-in 0.16s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-status-menu__item {
|
||||||
|
min-height: 38px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 9px;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0 10px;
|
||||||
|
outline: none;
|
||||||
|
color: $color-text-strong;
|
||||||
|
font-size: $font-size-13;
|
||||||
|
font-weight: 550;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
color: $gray50;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-highlighted] {
|
||||||
|
background: $color-surface-hover;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes device-status-menu-in {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-4px) scale(0.98);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0) scale(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -744,7 +891,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.device-page__grid {
|
.device-page__grid {
|
||||||
grid-template-columns: 540px minmax(360px, 1fr);;
|
grid-template-columns: 540px minmax(360px, 1fr);
|
||||||
|
;
|
||||||
}
|
}
|
||||||
|
|
||||||
.device-card--main {
|
.device-card--main {
|
||||||
|
|
@ -761,19 +909,23 @@
|
||||||
height: calc(100% - 36px);
|
height: calc(100% - 36px);
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
}
|
}
|
||||||
.device-card-stats{
|
|
||||||
|
.device-card-stats {
|
||||||
height: calc(100%);
|
height: calc(100%);
|
||||||
}
|
}
|
||||||
.device-impacts{
|
|
||||||
|
.device-impacts {
|
||||||
flex: 0;
|
flex: 0;
|
||||||
}
|
}
|
||||||
.device-battery{
|
|
||||||
|
.device-battery {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
.device-permissions{
|
|
||||||
|
.device-permissions {
|
||||||
height: calc(100% - 12px);
|
height: calc(100% - 12px);
|
||||||
padding: 8px 8px 4px 18px;
|
padding: 8px 8px 4px 18px;
|
||||||
}
|
}
|
||||||
|
|
@ -789,22 +941,27 @@
|
||||||
.device-map {
|
.device-map {
|
||||||
min-height: 252px;
|
min-height: 252px;
|
||||||
}
|
}
|
||||||
.history-period-tabs{
|
|
||||||
|
.history-period-tabs {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
.device-card__title{
|
|
||||||
|
.device-card__title {
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
svg{
|
|
||||||
|
svg {
|
||||||
width: 22px;
|
width: 22px;
|
||||||
height: 22px;
|
height: 22px;
|
||||||
}
|
}
|
||||||
h3{
|
|
||||||
|
h3 {
|
||||||
font-size: var(--font-size-16);
|
font-size: var(--font-size-16);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.device-card__header{
|
|
||||||
p{
|
.device-card__header {
|
||||||
font-size: var(--font-size-14);
|
p {
|
||||||
|
font-size: var(--font-size-14);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import {
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
useMemo,
|
useMemo,
|
||||||
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from 'react'
|
} from 'react'
|
||||||
import {
|
import {
|
||||||
|
|
@ -21,6 +22,7 @@ import { GET_DEVICE_PAGE_QUERY } from '../../entities/device/api/device.graphql'
|
||||||
import type {
|
import type {
|
||||||
Device as ApiDevice,
|
Device as ApiDevice,
|
||||||
DeviceNetworkStatus,
|
DeviceNetworkStatus,
|
||||||
|
DeviceTechnicalStatus,
|
||||||
GetPhoneGpsTrackData,
|
GetPhoneGpsTrackData,
|
||||||
GetTelemetryData,
|
GetTelemetryData,
|
||||||
GetDevicePageData,
|
GetDevicePageData,
|
||||||
|
|
@ -42,8 +44,10 @@ import { DevicePageSkeleton } from './components/DevicePageSkeleton/DevicePageSk
|
||||||
import type { AppLayoutOutletContext } from '../../app/layouts/AppLayout'
|
import type { AppLayoutOutletContext } from '../../app/layouts/AppLayout'
|
||||||
import {
|
import {
|
||||||
preloadOnIdle,
|
preloadOnIdle,
|
||||||
|
useDeferredModalPayload,
|
||||||
useLazyMount,
|
useLazyMount,
|
||||||
} from '../../shared/lib/lazyMount'
|
} from '../../shared/lib/lazyMount'
|
||||||
|
import { isPresent } from '../../shared/lib/isPresent'
|
||||||
|
|
||||||
const loadDeviceHistoryModal = () =>
|
const loadDeviceHistoryModal = () =>
|
||||||
import('./components/DeviceHistoryModal/DeviceHistoryModal').then(
|
import('./components/DeviceHistoryModal/DeviceHistoryModal').then(
|
||||||
|
|
@ -54,6 +58,26 @@ const loadDeviceHistoryModal = () =>
|
||||||
|
|
||||||
const DeviceHistoryModal = lazy(loadDeviceHistoryModal)
|
const DeviceHistoryModal = lazy(loadDeviceHistoryModal)
|
||||||
|
|
||||||
|
const loadDeviceMaintenanceModal = () =>
|
||||||
|
import('./components/DeviceMaintenanceModal/DeviceMaintenanceModal').then(
|
||||||
|
(module) => ({
|
||||||
|
default: module.DeviceMaintenanceModal,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
const DeviceMaintenanceModal = lazy(loadDeviceMaintenanceModal)
|
||||||
|
|
||||||
|
const loadDeviceMaintenanceHistoryModal = () =>
|
||||||
|
import(
|
||||||
|
'./components/DeviceMaintenanceHistoryModal/DeviceMaintenanceHistoryModal'
|
||||||
|
).then((module) => ({
|
||||||
|
default: module.DeviceMaintenanceHistoryModal,
|
||||||
|
}))
|
||||||
|
|
||||||
|
const DeviceMaintenanceHistoryModal = lazy(
|
||||||
|
loadDeviceMaintenanceHistoryModal,
|
||||||
|
)
|
||||||
|
|
||||||
function formatLocationDate(timestamp: number) {
|
function formatLocationDate(timestamp: number) {
|
||||||
if (!timestamp) return 'Нет данных'
|
if (!timestamp) return 'Нет данных'
|
||||||
|
|
||||||
|
|
@ -77,7 +101,7 @@ function getLatestTelemetryItem(telemetry: GetTelemetryData['getTelemetry']) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSortedGpsTrack(track: GetPhoneGpsTrackData['getPhoneGpsTrack']) {
|
function getSortedGpsTrack(track: GetPhoneGpsTrackData['getPhoneGpsTrack']) {
|
||||||
return [...track].sort((a, b) => a.date - b.date)
|
return track.filter(isPresent).sort((a, b) => a.date - b.date)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDeviceNetworkConnection(status?: DeviceNetworkStatus) {
|
function getDeviceNetworkConnection(status?: DeviceNetworkStatus) {
|
||||||
|
|
@ -94,6 +118,13 @@ function getDeviceNetworkLabel(status?: DeviceNetworkStatus) {
|
||||||
return 'Не в сети'
|
return 'Не в сети'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getDeviceCondition(status?: DeviceTechnicalStatus) {
|
||||||
|
if (status === 'InService') return 'service'
|
||||||
|
if (status === 'NeedsMaintenance') return 'inspection'
|
||||||
|
|
||||||
|
return 'ok'
|
||||||
|
}
|
||||||
|
|
||||||
function mapApiDeviceToPageDevice(
|
function mapApiDeviceToPageDevice(
|
||||||
device: ApiDevice,
|
device: ApiDevice,
|
||||||
batteryLevel?: number,
|
batteryLevel?: number,
|
||||||
|
|
@ -103,7 +134,6 @@ function mapApiDeviceToPageDevice(
|
||||||
const sortedGpsTrack = getSortedGpsTrack(gpsTrack)
|
const sortedGpsTrack = getSortedGpsTrack(gpsTrack)
|
||||||
const policy = device.policy
|
const policy = device.policy
|
||||||
const techState = device.techState
|
const techState = device.techState
|
||||||
const needMaintenance = techState?.needMaintenance ?? false
|
|
||||||
const bluetoothEnabled = isPhonePolicyOptionEnabled(
|
const bluetoothEnabled = isPhonePolicyOptionEnabled(
|
||||||
policy?.bluetooth ?? policy?.canUseBluetooth,
|
policy?.bluetooth ?? policy?.canUseBluetooth,
|
||||||
)
|
)
|
||||||
|
|
@ -157,7 +187,10 @@ function mapApiDeviceToPageDevice(
|
||||||
policy,
|
policy,
|
||||||
organisationPolicy: device.org?.policy ?? null,
|
organisationPolicy: device.org?.policy ?? null,
|
||||||
|
|
||||||
condition: needMaintenance ? 'inspection' : 'ok',
|
condition: getDeviceCondition(techState?.status),
|
||||||
|
technicalStatus: techState?.status ?? 'Healthy',
|
||||||
|
malfunctions: techState?.malfunctions ?? [],
|
||||||
|
malfunctionComment: techState?.malfunctionComment,
|
||||||
connection: getDeviceNetworkConnection(device.networkStatus),
|
connection: getDeviceNetworkConnection(device.networkStatus),
|
||||||
connectionText: getDeviceNetworkLabel(device.networkStatus),
|
connectionText: getDeviceNetworkLabel(device.networkStatus),
|
||||||
|
|
||||||
|
|
@ -217,12 +250,35 @@ export function DevicePage() {
|
||||||
const [searchParams, setSearchParams] = useSearchParams()
|
const [searchParams, setSearchParams] = useSearchParams()
|
||||||
const [isHistoryOpen, setIsHistoryOpen] = useState(false)
|
const [isHistoryOpen, setIsHistoryOpen] = useState(false)
|
||||||
const shouldMountHistoryModal = useLazyMount(isHistoryOpen)
|
const shouldMountHistoryModal = useLazyMount(isHistoryOpen)
|
||||||
|
const [maintenanceMode, setMaintenanceMode] = useState<
|
||||||
|
'view' | 'manage' | null
|
||||||
|
>(null)
|
||||||
|
const renderMaintenanceMode = useDeferredModalPayload(maintenanceMode)
|
||||||
|
const shouldMountMaintenanceModal = useLazyMount(maintenanceMode !== null)
|
||||||
|
const [isMaintenanceHistoryOpen, setIsMaintenanceHistoryOpen] =
|
||||||
|
useState(false)
|
||||||
|
const shouldMountMaintenanceHistoryModal = useLazyMount(
|
||||||
|
isMaintenanceHistoryOpen,
|
||||||
|
)
|
||||||
|
const maintenanceHistoryTimerRef = useRef<number | null>(null)
|
||||||
|
|
||||||
const numericDeviceId = Number(deviceId)
|
const numericDeviceId = Number(deviceId)
|
||||||
const isAppsView = searchParams.get('view') === 'apps'
|
const isAppsView = searchParams.get('view') === 'apps'
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return preloadOnIdle([loadDeviceHistoryModal])
|
return preloadOnIdle([
|
||||||
|
loadDeviceHistoryModal,
|
||||||
|
loadDeviceMaintenanceModal,
|
||||||
|
loadDeviceMaintenanceHistoryModal,
|
||||||
|
])
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (maintenanceHistoryTimerRef.current !== null) {
|
||||||
|
window.clearTimeout(maintenanceHistoryTimerRef.current)
|
||||||
|
}
|
||||||
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
|
@ -315,6 +371,35 @@ export function DevicePage() {
|
||||||
setIsHistoryOpen(true)
|
setIsHistoryOpen(true)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
const handleOpenMaintenanceDetails = useCallback(() => {
|
||||||
|
setMaintenanceMode('view')
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleManageMaintenance = useCallback(() => {
|
||||||
|
setMaintenanceMode('manage')
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleOpenMaintenanceHistory = useCallback(() => {
|
||||||
|
setMaintenanceMode(null)
|
||||||
|
|
||||||
|
if (maintenanceHistoryTimerRef.current !== null) {
|
||||||
|
window.clearTimeout(maintenanceHistoryTimerRef.current)
|
||||||
|
}
|
||||||
|
|
||||||
|
maintenanceHistoryTimerRef.current = window.setTimeout(() => {
|
||||||
|
setIsMaintenanceHistoryOpen(true)
|
||||||
|
maintenanceHistoryTimerRef.current = null
|
||||||
|
}, 220)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleOpenMaintenanceHistoryFromCard = useCallback(() => {
|
||||||
|
setIsMaintenanceHistoryOpen(true)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleMaintenanceUpdated = useCallback(async () => {
|
||||||
|
await refetch()
|
||||||
|
}, [refetch])
|
||||||
|
|
||||||
const handleRefetchPolicy = useCallback(async () => {
|
const handleRefetchPolicy = useCallback(async () => {
|
||||||
const result = await refetch()
|
const result = await refetch()
|
||||||
|
|
||||||
|
|
@ -372,12 +457,12 @@ export function DevicePage() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="device-page">
|
<section className={`device-page ${isAppsView ? 'device-page--apps' : ''}`}>
|
||||||
<AnimatePresence mode="wait" initial={false}>
|
<AnimatePresence mode="wait" initial={false}>
|
||||||
{isAppsView ? (
|
{isAppsView ? (
|
||||||
<m.div
|
<m.div
|
||||||
key="apps"
|
key="apps"
|
||||||
className="device-page__motion-view"
|
className="device-page__motion-view device-page__apps-motion"
|
||||||
variants={pageSwapVariants}
|
variants={pageSwapVariants}
|
||||||
initial="initial"
|
initial="initial"
|
||||||
animate="animate"
|
animate="animate"
|
||||||
|
|
@ -404,6 +489,9 @@ export function DevicePage() {
|
||||||
<DeviceMainCard
|
<DeviceMainCard
|
||||||
device={device}
|
device={device}
|
||||||
onOpenHistory={handleOpenHistory}
|
onOpenHistory={handleOpenHistory}
|
||||||
|
onOpenMaintenanceDetails={handleOpenMaintenanceDetails}
|
||||||
|
onManageMaintenance={handleManageMaintenance}
|
||||||
|
onOpenMaintenanceHistory={handleOpenMaintenanceHistoryFromCard}
|
||||||
/>
|
/>
|
||||||
<DeviceMapCard device={device} />
|
<DeviceMapCard device={device} />
|
||||||
|
|
||||||
|
|
@ -433,6 +521,29 @@ export function DevicePage() {
|
||||||
/>
|
/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
)}
|
)}
|
||||||
|
{shouldMountMaintenanceModal && renderMaintenanceMode && (
|
||||||
|
<Suspense fallback={null}>
|
||||||
|
<DeviceMaintenanceModal
|
||||||
|
open={maintenanceMode !== null}
|
||||||
|
mode={renderMaintenanceMode}
|
||||||
|
device={device}
|
||||||
|
onOpenHistory={handleOpenMaintenanceHistory}
|
||||||
|
onOpenChange={(nextOpen) => {
|
||||||
|
if (!nextOpen) setMaintenanceMode(null)
|
||||||
|
}}
|
||||||
|
onUpdated={handleMaintenanceUpdated}
|
||||||
|
/>
|
||||||
|
</Suspense>
|
||||||
|
)}
|
||||||
|
{shouldMountMaintenanceHistoryModal && (
|
||||||
|
<Suspense fallback={null}>
|
||||||
|
<DeviceMaintenanceHistoryModal
|
||||||
|
open={isMaintenanceHistoryOpen}
|
||||||
|
device={device}
|
||||||
|
onOpenChange={setIsMaintenanceHistoryOpen}
|
||||||
|
/>
|
||||||
|
</Suspense>
|
||||||
|
)}
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,13 @@
|
||||||
@use '../../../../shared/styles/variables' as *;
|
@use '../../../../shared/styles/variables' as *;
|
||||||
|
|
||||||
.device-apps-view {
|
.device-apps-view {
|
||||||
display: flex;
|
display: grid;
|
||||||
flex: 1;
|
grid-template-rows: auto auto minmax(0, 1fr);
|
||||||
|
flex: 1 1 0;
|
||||||
|
min-width: 0;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
flex-direction: column;
|
height: 100%;
|
||||||
|
max-height: 100%;
|
||||||
gap: 14px;
|
gap: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -237,16 +240,43 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.device-apps-view__panel {
|
.device-apps-view__panel {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
flex: 1;
|
height: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
flex: 1 1 0;
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
background: $color-surface;
|
background: $color-surface;
|
||||||
box-shadow: 5px 5px 40px -10px rgba($gray50-rgb, 0.2);
|
box-shadow: 5px 5px 40px -10px rgba($gray50-rgb, 0.2);
|
||||||
overflow: hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.device-apps-view__scroll {
|
.device-apps-view__scroll {
|
||||||
|
position: relative;
|
||||||
|
flex: 1 1 0;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 0;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
border-radius: 20px;
|
||||||
|
background: $color-surface;
|
||||||
|
|
||||||
|
.simplebar-wrapper,
|
||||||
|
.simplebar-mask,
|
||||||
|
.simplebar-offset,
|
||||||
|
.simplebar-content-wrapper {
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 0;
|
||||||
|
max-width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.simplebar-content-wrapper {
|
||||||
|
border-radius: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
.simplebar-content {
|
.simplebar-content {
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
|
|
@ -254,18 +284,29 @@
|
||||||
|
|
||||||
.simplebar-track.simplebar-vertical {
|
.simplebar-track.simplebar-vertical {
|
||||||
top: 14px;
|
top: 14px;
|
||||||
right: 6px;
|
right: -7px;
|
||||||
bottom: 14px;
|
bottom: 14px;
|
||||||
width: 8px;
|
width: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.simplebar-scrollbar::before {
|
.simplebar-scrollbar::before {
|
||||||
background: $gray50;
|
background: color-mix(in srgb, var(--gray50) 85%, transparent);
|
||||||
border-radius: 4px;
|
border-radius: 6px;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.device-apps-view__reveal {
|
||||||
|
display: block;
|
||||||
|
min-height: 100%;
|
||||||
|
|
||||||
|
.reveal-content__fallback,
|
||||||
|
.reveal-content__content {
|
||||||
|
display: block;
|
||||||
|
min-height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.device-apps-view__state {
|
.device-apps-view__state {
|
||||||
min-height: 220px;
|
min-height: 220px;
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
|
|
|
||||||
|
|
@ -483,7 +483,11 @@ export function DeviceAppsView({
|
||||||
|
|
||||||
<div className="device-apps-view__panel">
|
<div className="device-apps-view__panel">
|
||||||
<SimpleBar className="device-apps-view__scroll">
|
<SimpleBar className="device-apps-view__scroll">
|
||||||
<RevealContent loading={loading} fallback={<ListSkeleton />}>
|
<RevealContent
|
||||||
|
className="device-apps-view__reveal"
|
||||||
|
loading={loading}
|
||||||
|
fallback={<ListSkeleton />}
|
||||||
|
>
|
||||||
{error && (
|
{error && (
|
||||||
<div className="device-apps-view__state is-error">
|
<div className="device-apps-view__state is-error">
|
||||||
Не удалось загрузить список приложений
|
Не удалось загрузить список приложений
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ import type {
|
||||||
DevicePackageUseEventsGroup,
|
DevicePackageUseEventsGroup,
|
||||||
DevicePhoneStateEventType
|
DevicePhoneStateEventType
|
||||||
} from '../../../../entities/device/model/types'
|
} from '../../../../entities/device/model/types'
|
||||||
|
import { isPresent } from '../../../../shared/lib/isPresent'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DeviceHistoryPeriodControl,
|
DeviceHistoryPeriodControl,
|
||||||
|
|
@ -194,7 +195,7 @@ export function DeviceHistoryModal({
|
||||||
return [
|
return [
|
||||||
...telemetryData.getTelemetry,
|
...telemetryData.getTelemetry,
|
||||||
|
|
||||||
...telemetryData.getPhoneStateEvents.map(
|
...telemetryData.getPhoneStateEvents.filter(isPresent).map(
|
||||||
(event): DeviceHistoryChartItem => ({
|
(event): DeviceHistoryChartItem => ({
|
||||||
date: event.date,
|
date: event.date,
|
||||||
type: 'event',
|
type: 'event',
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,35 @@
|
||||||
import { memo, useState } from 'react'
|
import { memo, useState } from 'react'
|
||||||
import { AnimatePresence, m, useReducedMotion } from 'framer-motion'
|
import { AnimatePresence, m, useReducedMotion } from 'framer-motion'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
|
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
||||||
import {
|
import {
|
||||||
Building2,
|
Building2,
|
||||||
|
CircleAlert,
|
||||||
|
Eye,
|
||||||
|
History,
|
||||||
Lock,
|
Lock,
|
||||||
|
Menu,
|
||||||
|
Pencil,
|
||||||
ShieldCheck,
|
ShieldCheck,
|
||||||
Signal,
|
Signal,
|
||||||
Smartphone,
|
Smartphone,
|
||||||
Trash2,
|
Trash2,
|
||||||
|
Wrench,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
|
|
||||||
import type { Device } from '../../types'
|
import type { Device } from '../../types'
|
||||||
import { conditionText, connectionText, getStatusClass } from '../../types'
|
import { conditionText, connectionText, getStatusClass } from '../../types'
|
||||||
import { ConfirmDangerDialog } from '../../../../widgets/ConfirmDangerDialog/ConfirmDangerDialog'
|
import { ConfirmDangerDialog } from '../../../../widgets/ConfirmDangerDialog/ConfirmDangerDialog'
|
||||||
import { useDeferredModalPayload } from '../../../../shared/lib/lazyMount'
|
import { useDeferredModalPayload } from '../../../../shared/lib/lazyMount'
|
||||||
|
import { getDeviceMalfunctionSummary } from '../../../../entities/device/lib/maintenance'
|
||||||
|
import { Tooltip } from '../../../../shared/ui/Tooltip/Tooltip'
|
||||||
|
|
||||||
type DeviceMainCardProps = {
|
type DeviceMainCardProps = {
|
||||||
device: Device
|
device: Device
|
||||||
onOpenHistory: () => void
|
onOpenHistory: () => void
|
||||||
|
onOpenMaintenanceDetails: () => void
|
||||||
|
onManageMaintenance: () => void
|
||||||
|
onOpenMaintenanceHistory: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDeviceFullName(device: Device) {
|
function getDeviceFullName(device: Device) {
|
||||||
|
|
@ -30,6 +42,9 @@ function areDeviceMainCardPropsEqual(
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
prev.onOpenHistory === next.onOpenHistory &&
|
prev.onOpenHistory === next.onOpenHistory &&
|
||||||
|
prev.onOpenMaintenanceDetails === next.onOpenMaintenanceDetails &&
|
||||||
|
prev.onManageMaintenance === next.onManageMaintenance &&
|
||||||
|
prev.onOpenMaintenanceHistory === next.onOpenMaintenanceHistory &&
|
||||||
prev.device.id === next.device.id &&
|
prev.device.id === next.device.id &&
|
||||||
prev.device.image === next.device.image &&
|
prev.device.image === next.device.image &&
|
||||||
prev.device.model === next.device.model &&
|
prev.device.model === next.device.model &&
|
||||||
|
|
@ -38,6 +53,9 @@ function areDeviceMainCardPropsEqual(
|
||||||
prev.device.imei === next.device.imei &&
|
prev.device.imei === next.device.imei &&
|
||||||
prev.device.imei2 === next.device.imei2 &&
|
prev.device.imei2 === next.device.imei2 &&
|
||||||
prev.device.condition === next.device.condition &&
|
prev.device.condition === next.device.condition &&
|
||||||
|
prev.device.technicalStatus === next.device.technicalStatus &&
|
||||||
|
prev.device.malfunctionComment === next.device.malfunctionComment &&
|
||||||
|
prev.device.malfunctions.join(',') === next.device.malfunctions.join(',') &&
|
||||||
prev.device.connection === next.device.connection &&
|
prev.device.connection === next.device.connection &&
|
||||||
prev.device.connectionText === next.device.connectionText &&
|
prev.device.connectionText === next.device.connectionText &&
|
||||||
prev.device.permissions?.locked === next.device.permissions?.locked &&
|
prev.device.permissions?.locked === next.device.permissions?.locked &&
|
||||||
|
|
@ -51,6 +69,9 @@ function areDeviceMainCardPropsEqual(
|
||||||
export const DeviceMainCard = memo(function DeviceMainCard({
|
export const DeviceMainCard = memo(function DeviceMainCard({
|
||||||
device,
|
device,
|
||||||
onOpenHistory,
|
onOpenHistory,
|
||||||
|
onOpenMaintenanceDetails,
|
||||||
|
onManageMaintenance,
|
||||||
|
onOpenMaintenanceHistory,
|
||||||
}: DeviceMainCardProps) {
|
}: DeviceMainCardProps) {
|
||||||
const [deletingDevice, setDeletingDevice] = useState<Device | null>(null)
|
const [deletingDevice, setDeletingDevice] = useState<Device | null>(null)
|
||||||
const renderDeletingDevice = useDeferredModalPayload(deletingDevice)
|
const renderDeletingDevice = useDeferredModalPayload(deletingDevice)
|
||||||
|
|
@ -61,12 +82,32 @@ export const DeviceMainCard = memo(function DeviceMainCard({
|
||||||
duration: 0.24,
|
duration: 0.24,
|
||||||
ease: [0.34, 1.42, 0.64, 1] as [number, number, number, number],
|
ease: [0.34, 1.42, 0.64, 1] as [number, number, number, number],
|
||||||
}
|
}
|
||||||
const statusItems = [
|
const statusItems: Array<{
|
||||||
|
key: string
|
||||||
|
className: string
|
||||||
|
icon: React.ReactNode
|
||||||
|
content: string
|
||||||
|
title?: string
|
||||||
|
isTechnical?: boolean
|
||||||
|
}> = [
|
||||||
{
|
{
|
||||||
key: `condition-${device.condition}`,
|
key: `condition-${device.condition}`,
|
||||||
className: getStatusClass(device.condition),
|
className: getStatusClass(device.condition),
|
||||||
icon: <ShieldCheck size={17} />,
|
icon:
|
||||||
|
device.condition === 'ok' ? (
|
||||||
|
<ShieldCheck size={17} />
|
||||||
|
) : device.condition === 'service' ? (
|
||||||
|
<Wrench size={17} />
|
||||||
|
) : (
|
||||||
|
<CircleAlert size={17} />
|
||||||
|
),
|
||||||
content: conditionText[device.condition],
|
content: conditionText[device.condition],
|
||||||
|
title:
|
||||||
|
getDeviceMalfunctionSummary(
|
||||||
|
device.malfunctions,
|
||||||
|
device.malfunctionComment,
|
||||||
|
) || undefined,
|
||||||
|
isTechnical: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: `connection-${device.connection}-${device.connectionText ?? ''}`,
|
key: `connection-${device.connection}-${device.connectionText ?? ''}`,
|
||||||
|
|
@ -133,7 +174,7 @@ export const DeviceMainCard = memo(function DeviceMainCard({
|
||||||
<m.div
|
<m.div
|
||||||
key={status.key}
|
key={status.key}
|
||||||
layout={!shouldReduceMotion}
|
layout={!shouldReduceMotion}
|
||||||
className={`device-status ${status.className}`}
|
className="device-status-row"
|
||||||
initial={{ opacity: 0, x: shouldReduceMotion ? 0 : -8 }}
|
initial={{ opacity: 0, x: shouldReduceMotion ? 0 : -8 }}
|
||||||
animate={{
|
animate={{
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
|
|
@ -149,8 +190,67 @@ export const DeviceMainCard = memo(function DeviceMainCard({
|
||||||
transition: statusTransition,
|
transition: statusTransition,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{status.icon}
|
<Tooltip content={status.title}>
|
||||||
{status.content}
|
{status.isTechnical ? (
|
||||||
|
<button
|
||||||
|
className={`device-status device-status--interactive ${status.className}`}
|
||||||
|
type="button"
|
||||||
|
onClick={onOpenMaintenanceDetails}
|
||||||
|
>
|
||||||
|
{status.icon}
|
||||||
|
{status.content}
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<span className={`device-status ${status.className}`}>
|
||||||
|
{status.icon}
|
||||||
|
{status.content}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
{status.isTechnical && (
|
||||||
|
<DropdownMenu.Root>
|
||||||
|
<DropdownMenu.Trigger asChild>
|
||||||
|
<button
|
||||||
|
className="device-status-menu__trigger"
|
||||||
|
type="button"
|
||||||
|
aria-label="Действия с техническим состоянием"
|
||||||
|
>
|
||||||
|
<Menu size={16} />
|
||||||
|
</button>
|
||||||
|
</DropdownMenu.Trigger>
|
||||||
|
|
||||||
|
<DropdownMenu.Portal>
|
||||||
|
<DropdownMenu.Content
|
||||||
|
className="device-status-menu"
|
||||||
|
align="start"
|
||||||
|
sideOffset={7}
|
||||||
|
>
|
||||||
|
<DropdownMenu.Item
|
||||||
|
className="device-status-menu__item"
|
||||||
|
onSelect={onOpenMaintenanceDetails}
|
||||||
|
>
|
||||||
|
<Eye size={16} />
|
||||||
|
Просмотреть состояние
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Item
|
||||||
|
className="device-status-menu__item"
|
||||||
|
onSelect={onManageMaintenance}
|
||||||
|
>
|
||||||
|
<Pencil size={16} />
|
||||||
|
Изменить состояние
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Item
|
||||||
|
className="device-status-menu__item"
|
||||||
|
onSelect={onOpenMaintenanceHistory}
|
||||||
|
>
|
||||||
|
<History size={16} />
|
||||||
|
Журнал обслуживания
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
</DropdownMenu.Content>
|
||||||
|
</DropdownMenu.Portal>
|
||||||
|
</DropdownMenu.Root>
|
||||||
|
)}
|
||||||
</m.div>
|
</m.div>
|
||||||
))}
|
))}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,235 @@
|
||||||
|
@use '../../../../shared/styles/variables' as *;
|
||||||
|
|
||||||
|
.device-maintenance-history-modal__overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 500;
|
||||||
|
background: $color-backdrop;
|
||||||
|
backdrop-filter: blur(3px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-maintenance-history-modal {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 510;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
width: min(860px, calc(100vw - 32px));
|
||||||
|
height: min(760px, calc(100vh - 32px));
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 24px;
|
||||||
|
background: $color-surface;
|
||||||
|
box-shadow: $shadow-modal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-maintenance-history-modal__header {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 20px;
|
||||||
|
padding: 22px 24px 18px;
|
||||||
|
border-bottom: 1px solid $color-border-muted;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin: 0;
|
||||||
|
color: $color-text-strong;
|
||||||
|
font-size: $font-size-24;
|
||||||
|
font-weight: 650;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 5px 0 0;
|
||||||
|
color: $color-text-muted;
|
||||||
|
font-size: $font-size-14;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-maintenance-history-modal__close {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
width: 38px;
|
||||||
|
height: 38px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 11px;
|
||||||
|
background: $color-bg;
|
||||||
|
color: $color-text-muted;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: 0.2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: $color-surface-hover;
|
||||||
|
color: $color-primary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-maintenance-history-modal__scroll {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-maintenance-history-modal__body {
|
||||||
|
padding: 20px 24px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-maintenance-history {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-maintenance-history-case {
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid $color-border-muted;
|
||||||
|
border-radius: 15px;
|
||||||
|
background: $color-surface-muted;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-radix-collection-item] {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
padding: 13px 14px;
|
||||||
|
color: $color-text-strong;
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
> span {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
b {
|
||||||
|
font-size: $font-size-14;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
small {
|
||||||
|
color: $color-text-muted;
|
||||||
|
font-size: $font-size-12;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
color: $color-text-muted;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-state='open'] svg {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-maintenance-history-case__details {
|
||||||
|
padding: 0 14px 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-maintenance-history-case__malfunctions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
|
||||||
|
span {
|
||||||
|
border-radius: 12px;
|
||||||
|
background: $color-danger-bg;
|
||||||
|
padding: 4px 10px;
|
||||||
|
color: $color-danger;
|
||||||
|
font-size: $font-size-12;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-maintenance-history-case__comment {
|
||||||
|
margin-top: 11px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-radius: 11px;
|
||||||
|
background: $color-surface;
|
||||||
|
|
||||||
|
span {
|
||||||
|
color: $color-text-muted;
|
||||||
|
font-size: $font-size-11;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 3px 0 0;
|
||||||
|
color: $color-text;
|
||||||
|
font-size: $font-size-13;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-maintenance-history-case__dates {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px 18px;
|
||||||
|
margin-top: 12px;
|
||||||
|
color: $color-text-muted;
|
||||||
|
font-size: $font-size-12;
|
||||||
|
|
||||||
|
b {
|
||||||
|
color: $color-text;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-maintenance-history-case__metrics {
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-maintenance-history-modal__message {
|
||||||
|
padding: 28px;
|
||||||
|
border-radius: 15px;
|
||||||
|
background: $color-surface-muted;
|
||||||
|
color: $color-text-muted;
|
||||||
|
font-size: $font-size-14;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
&.is-error {
|
||||||
|
background: $color-danger-bg;
|
||||||
|
color: $color-danger;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-maintenance-history-modal__footer {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
min-height: 72px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding: 14px 24px;
|
||||||
|
border-top: 1px solid $color-border-muted;
|
||||||
|
background: $color-surface;
|
||||||
|
|
||||||
|
button {
|
||||||
|
min-height: 42px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: $color-bg;
|
||||||
|
padding: 0 18px;
|
||||||
|
color: $color-text-strong;
|
||||||
|
font-size: $font-size-14;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: 0.18s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: $color-surface-hover;
|
||||||
|
color: $color-primary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,193 @@
|
||||||
|
import { useMemo } from 'react'
|
||||||
|
import * as Accordion from '@radix-ui/react-accordion'
|
||||||
|
import * as Dialog from '@radix-ui/react-dialog'
|
||||||
|
import { useQuery } from '@apollo/client/react'
|
||||||
|
import SimpleBar from 'simplebar-react'
|
||||||
|
import {
|
||||||
|
ChevronDown,
|
||||||
|
X,
|
||||||
|
} from 'lucide-react'
|
||||||
|
|
||||||
|
import { GET_PHONE_MAINTENANCE_HISTORY_QUERY } from '../../../../entities/device/api/device.graphql'
|
||||||
|
import {
|
||||||
|
deviceMalfunctionLabels,
|
||||||
|
isRedundantMalfunctionComment,
|
||||||
|
} from '../../../../entities/device/lib/maintenance'
|
||||||
|
import type {
|
||||||
|
GetPhoneMaintenanceHistoryData,
|
||||||
|
GetPhoneMaintenanceHistoryVariables,
|
||||||
|
} from '../../../../entities/device/model/types'
|
||||||
|
import { MotionDialogContent } from '../../../../shared/ui/MotionDialog/MotionDialog'
|
||||||
|
import type { Device } from '../../types'
|
||||||
|
import { DeviceMaintenanceMetrics } from '../DeviceMaintenanceMetrics/DeviceMaintenanceMetrics'
|
||||||
|
|
||||||
|
import './DeviceMaintenanceHistoryModal.scss'
|
||||||
|
|
||||||
|
type DeviceMaintenanceHistoryModalProps = {
|
||||||
|
open: boolean
|
||||||
|
device: Device
|
||||||
|
onOpenChange: (open: boolean) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
type MaintenanceCase =
|
||||||
|
GetPhoneMaintenanceHistoryData['getPhoneMaintenanceHistory'][number]
|
||||||
|
|
||||||
|
const maintenanceStatusLabels = {
|
||||||
|
Open: 'Ожидает обслуживания',
|
||||||
|
InService: 'На обслуживании',
|
||||||
|
Closed: 'Обслуживание завершено',
|
||||||
|
} as const
|
||||||
|
|
||||||
|
function formatDate(value?: number | null) {
|
||||||
|
if (!value) return 'Не указано'
|
||||||
|
|
||||||
|
return new Intl.DateTimeFormat('ru-RU', {
|
||||||
|
day: '2-digit',
|
||||||
|
month: '2-digit',
|
||||||
|
year: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
}).format(new Date(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
function CaseDetails({ item }: { item: MaintenanceCase }) {
|
||||||
|
const showComment = !isRedundantMalfunctionComment(
|
||||||
|
item.malfunctions,
|
||||||
|
item.comment,
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="device-maintenance-history-case__details">
|
||||||
|
<div className="device-maintenance-history-case__malfunctions">
|
||||||
|
{item.malfunctions.map((malfunction) => (
|
||||||
|
<span key={malfunction}>{deviceMalfunctionLabels[malfunction]}</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{showComment && (
|
||||||
|
<div className="device-maintenance-history-case__comment">
|
||||||
|
<span>Комментарий</span>
|
||||||
|
<p>{item.comment}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="device-maintenance-history-case__dates">
|
||||||
|
<span>Обнаружено: <b>{formatDate(item.detectedAt)}</b></span>
|
||||||
|
{item.serviceStartedAt && (
|
||||||
|
<span>Передано в сервис: <b>{formatDate(item.serviceStartedAt)}</b></span>
|
||||||
|
)}
|
||||||
|
{item.closedAt && (
|
||||||
|
<span>Возвращено: <b>{formatDate(item.closedAt)}</b></span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="device-maintenance-history-case__metrics">
|
||||||
|
<DeviceMaintenanceMetrics metrics={item.metrics} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DeviceMaintenanceHistoryModal({
|
||||||
|
open,
|
||||||
|
device,
|
||||||
|
onOpenChange,
|
||||||
|
}: DeviceMaintenanceHistoryModalProps) {
|
||||||
|
const { data, loading, error } = useQuery<
|
||||||
|
GetPhoneMaintenanceHistoryData,
|
||||||
|
GetPhoneMaintenanceHistoryVariables
|
||||||
|
>(GET_PHONE_MAINTENANCE_HISTORY_QUERY, {
|
||||||
|
variables: { phoneId: String(device.id) },
|
||||||
|
skip: !open,
|
||||||
|
fetchPolicy: 'network-only',
|
||||||
|
})
|
||||||
|
|
||||||
|
const history = useMemo(
|
||||||
|
() => [...(data?.getPhoneMaintenanceHistory ?? [])]
|
||||||
|
.sort((a, b) => b.detectedAt - a.detectedAt),
|
||||||
|
[data],
|
||||||
|
)
|
||||||
|
const activeCase = history.find((item) => item.status !== 'Closed') ?? null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog.Root open={open} onOpenChange={onOpenChange}>
|
||||||
|
<MotionDialogContent
|
||||||
|
open={open}
|
||||||
|
overlayClassName="device-maintenance-history-modal__overlay"
|
||||||
|
contentClassName="device-maintenance-history-modal"
|
||||||
|
motionPreset="fade"
|
||||||
|
>
|
||||||
|
<header className="device-maintenance-history-modal__header">
|
||||||
|
<div>
|
||||||
|
<Dialog.Title>Журнал обслуживания</Dialog.Title>
|
||||||
|
<Dialog.Description>
|
||||||
|
{device.factoryNumber} · ID {device.id}
|
||||||
|
</Dialog.Description>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Dialog.Close asChild>
|
||||||
|
<button
|
||||||
|
className="device-maintenance-history-modal__close"
|
||||||
|
type="button"
|
||||||
|
aria-label="Закрыть"
|
||||||
|
>
|
||||||
|
<X size={20} />
|
||||||
|
</button>
|
||||||
|
</Dialog.Close>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<SimpleBar className="device-maintenance-history-modal__scroll">
|
||||||
|
<div className="device-maintenance-history-modal__body">
|
||||||
|
{loading && history.length === 0 ? (
|
||||||
|
<div className="device-maintenance-history-modal__message">
|
||||||
|
Загружаем журнал...
|
||||||
|
</div>
|
||||||
|
) : error ? (
|
||||||
|
<div className="device-maintenance-history-modal__message is-error">
|
||||||
|
Не удалось загрузить журнал обслуживания.
|
||||||
|
</div>
|
||||||
|
) : history.length > 0 ? (
|
||||||
|
<Accordion.Root
|
||||||
|
className="device-maintenance-history"
|
||||||
|
type="single"
|
||||||
|
defaultValue={activeCase ? String(activeCase.id) : undefined}
|
||||||
|
collapsible
|
||||||
|
>
|
||||||
|
{history.map((item) => (
|
||||||
|
<Accordion.Item
|
||||||
|
className="device-maintenance-history-case"
|
||||||
|
value={String(item.id)}
|
||||||
|
key={item.id}
|
||||||
|
>
|
||||||
|
<Accordion.Header>
|
||||||
|
<Accordion.Trigger>
|
||||||
|
<span>
|
||||||
|
<b>{maintenanceStatusLabels[item.status]}</b>
|
||||||
|
<small>{formatDate(item.detectedAt)}</small>
|
||||||
|
</span>
|
||||||
|
<ChevronDown size={18} />
|
||||||
|
</Accordion.Trigger>
|
||||||
|
</Accordion.Header>
|
||||||
|
<Accordion.Content>
|
||||||
|
<CaseDetails item={item} />
|
||||||
|
</Accordion.Content>
|
||||||
|
</Accordion.Item>
|
||||||
|
))}
|
||||||
|
</Accordion.Root>
|
||||||
|
) : (
|
||||||
|
<div className="device-maintenance-history-modal__message">
|
||||||
|
История обслуживания пока пуста.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</SimpleBar>
|
||||||
|
|
||||||
|
<footer className="device-maintenance-history-modal__footer">
|
||||||
|
<Dialog.Close asChild>
|
||||||
|
<button type="button">Закрыть</button>
|
||||||
|
</Dialog.Close>
|
||||||
|
</footer>
|
||||||
|
</MotionDialogContent>
|
||||||
|
</Dialog.Root>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
@use '../../../../shared/styles/variables' as *;
|
||||||
|
|
||||||
|
.device-maintenance-metrics {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
gap: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-maintenance-metric {
|
||||||
|
min-width: 0;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 22px minmax(0, 1fr) auto;
|
||||||
|
align-items: center;
|
||||||
|
gap: 7px;
|
||||||
|
padding: 9px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: $color-surface;
|
||||||
|
color: $color-text-muted;
|
||||||
|
font-size: $font-size-12;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
color: $color-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
b {
|
||||||
|
color: $color-text-strong;
|
||||||
|
font-weight: 650;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 760px) {
|
||||||
|
.device-maintenance-metrics {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
import {
|
||||||
|
BatteryMedium,
|
||||||
|
Clock3,
|
||||||
|
Hammer,
|
||||||
|
RotateCcw,
|
||||||
|
Thermometer,
|
||||||
|
} from 'lucide-react'
|
||||||
|
|
||||||
|
import './DeviceMaintenanceMetrics.scss'
|
||||||
|
|
||||||
|
export type DeviceMaintenanceMetricsValue = {
|
||||||
|
hits: number
|
||||||
|
overheats: number
|
||||||
|
batteryCycles: number
|
||||||
|
worktime: number
|
||||||
|
batteryCapacity?: number | null
|
||||||
|
}
|
||||||
|
|
||||||
|
type DeviceMaintenanceMetricsProps = {
|
||||||
|
metrics: DeviceMaintenanceMetricsValue
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatWorktime(value: number) {
|
||||||
|
if (!Number.isFinite(value)) return '-'
|
||||||
|
|
||||||
|
const hours = value / 1000 / 60 / 60
|
||||||
|
|
||||||
|
if (hours < 1) {
|
||||||
|
return `${Math.round(value / 1000 / 60).toLocaleString('ru-RU')} мин`
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${(Math.round(hours * 10) / 10).toLocaleString('ru-RU')} ч`
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatMetric(value?: number | null, suffix = '') {
|
||||||
|
if (value === null || value === undefined) return '-'
|
||||||
|
return `${value.toLocaleString('ru-RU')}${suffix}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DeviceMaintenanceMetrics({
|
||||||
|
metrics,
|
||||||
|
}: DeviceMaintenanceMetricsProps) {
|
||||||
|
const items = [
|
||||||
|
{
|
||||||
|
label: 'Удары',
|
||||||
|
value: formatMetric(metrics.hits),
|
||||||
|
icon: <Hammer size={17} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Перегревы',
|
||||||
|
value: formatMetric(metrics.overheats),
|
||||||
|
icon: <Thermometer size={17} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Циклы зарядки',
|
||||||
|
value: formatMetric(metrics.batteryCycles),
|
||||||
|
icon: <RotateCcw size={17} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Время работы',
|
||||||
|
value: formatWorktime(metrics.worktime),
|
||||||
|
icon: <Clock3 size={17} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Ёмкость АКБ',
|
||||||
|
value: formatMetric(metrics.batteryCapacity, '%'),
|
||||||
|
icon: <BatteryMedium size={17} />,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="device-maintenance-metrics">
|
||||||
|
{items.map((item) => (
|
||||||
|
<div className="device-maintenance-metric" key={item.label}>
|
||||||
|
{item.icon}
|
||||||
|
<span>{item.label}</span>
|
||||||
|
<b>{item.value}</b>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,421 @@
|
||||||
|
@use '../../../../shared/styles/variables' as *;
|
||||||
|
|
||||||
|
.device-maintenance-modal__overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 500;
|
||||||
|
background: $color-backdrop;
|
||||||
|
backdrop-filter: blur(3px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-maintenance-modal {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 510;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
width: min(860px, calc(100vw - 32px));
|
||||||
|
max-height: calc(100vh - 32px);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 24px;
|
||||||
|
background: $color-surface;
|
||||||
|
box-shadow: $shadow-modal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-maintenance-modal__header {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 20px;
|
||||||
|
padding: 22px 24px 18px;
|
||||||
|
border-bottom: 1px solid $color-border-muted;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin: 0;
|
||||||
|
color: $color-text-strong;
|
||||||
|
font-size: $font-size-24;
|
||||||
|
font-weight: 650;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 5px 0 0;
|
||||||
|
color: $color-text-muted;
|
||||||
|
font-size: $font-size-14;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-maintenance-modal__close {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
width: 38px;
|
||||||
|
height: 38px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 11px;
|
||||||
|
background: $color-bg;
|
||||||
|
color: $color-text-muted;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: 0.2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: $color-surface-hover;
|
||||||
|
color: $color-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-maintenance-modal__scroll {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-height: 0;
|
||||||
|
max-height: calc(100vh - 190px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-maintenance-modal__body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 20px 24px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-maintenance-summary {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 44px minmax(170px, auto) minmax(0, 1fr);
|
||||||
|
align-items: center;
|
||||||
|
gap: 14px;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 16px;
|
||||||
|
background: $color-surface-muted;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-maintenance-summary__icon {
|
||||||
|
width: 42px;
|
||||||
|
height: 42px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 13px;
|
||||||
|
background: $color-bg;
|
||||||
|
color: $color-text-muted;
|
||||||
|
|
||||||
|
&.is-success {
|
||||||
|
background: $color-success-bg;
|
||||||
|
color: $color-success;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-danger {
|
||||||
|
background: $color-danger-bg;
|
||||||
|
color: $color-danger;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-service {
|
||||||
|
background: $color-primary-muted;
|
||||||
|
color: $color-primary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-maintenance-summary__field {
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
line-height: 1;
|
||||||
|
|
||||||
|
> span {
|
||||||
|
color: $color-text-muted;
|
||||||
|
font-size: $font-size-12;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
strong {
|
||||||
|
color: $color-text-strong;
|
||||||
|
font-size: $font-size-17;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
color: $color-text;
|
||||||
|
font-size: $font-size-13;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-maintenance-summary__comment {
|
||||||
|
align-self: stretch;
|
||||||
|
justify-content: center;
|
||||||
|
padding-left: 16px;
|
||||||
|
border-left: 1px solid $color-border-muted;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-maintenance-section {
|
||||||
|
padding: 16px;
|
||||||
|
border: 1px solid $color-border-muted;
|
||||||
|
border-radius: 18px;
|
||||||
|
background: $color-surface;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-maintenance-section__heading {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 0;
|
||||||
|
color: $color-text-strong;
|
||||||
|
font-size: $font-size-16;
|
||||||
|
font-weight: 650;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
color: $color-text-muted;
|
||||||
|
font-size: $font-size-13;
|
||||||
|
}
|
||||||
|
|
||||||
|
> svg {
|
||||||
|
color: $color-text-muted;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-maintenance-current-malfunctions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
|
||||||
|
span {
|
||||||
|
border-radius: 12px;
|
||||||
|
background: $color-danger-bg;
|
||||||
|
padding: 4px 10px;
|
||||||
|
color: $color-danger;
|
||||||
|
font-size: $font-size-12;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-maintenance-metrics-message {
|
||||||
|
margin: 0;
|
||||||
|
color: $color-text-muted;
|
||||||
|
font-size: $font-size-13;
|
||||||
|
|
||||||
|
&.is-error {
|
||||||
|
color: $color-danger;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-maintenance-options {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-maintenance-option {
|
||||||
|
min-width: 0;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 22px minmax(0, 1fr);
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 11px;
|
||||||
|
border: 1px solid $color-border-muted;
|
||||||
|
border-radius: 13px;
|
||||||
|
background: $color-surface-muted;
|
||||||
|
color: $color-text;
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: 0.18s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: $color-border-accent;
|
||||||
|
background: $color-surface-hover;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-selected {
|
||||||
|
border-color: $color-primary;
|
||||||
|
background: $color-primary-tint-soft;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.65;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
> span:last-child {
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
b {
|
||||||
|
color: $color-text-strong;
|
||||||
|
font-size: $font-size-14;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
small {
|
||||||
|
color: $color-text-muted;
|
||||||
|
font-size: $font-size-12;
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-maintenance-option__check {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: 1px solid $color-border-strong;
|
||||||
|
border-radius: 7px;
|
||||||
|
background: $color-surface;
|
||||||
|
color: $color-text-inverse;
|
||||||
|
|
||||||
|
.is-selected & {
|
||||||
|
border-color: $color-primary;
|
||||||
|
background: $color-primary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-maintenance-comment {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 7px;
|
||||||
|
margin-top: 12px;
|
||||||
|
|
||||||
|
> span {
|
||||||
|
color: $color-text-strong;
|
||||||
|
font-size: $font-size-13;
|
||||||
|
font-weight: 600;
|
||||||
|
|
||||||
|
b {
|
||||||
|
color: $color-danger;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
min-height: 78px;
|
||||||
|
resize: vertical;
|
||||||
|
border: 1px solid $color-border-strong;
|
||||||
|
border-radius: 13px;
|
||||||
|
background: $color-surface-muted;
|
||||||
|
padding: 11px 13px;
|
||||||
|
outline: none;
|
||||||
|
color: $color-text-strong;
|
||||||
|
font: inherit;
|
||||||
|
font-size: $font-size-14;
|
||||||
|
transition: 0.18s ease;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border-color: $color-primary;
|
||||||
|
box-shadow: 0 0 0 3px $color-primary-focus;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: $color-text-soft;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-maintenance-modal__footer {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
min-height: 72px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 14px 24px;
|
||||||
|
border-top: 1px solid $color-border-muted;
|
||||||
|
background: $color-surface;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-maintenance-modal__actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-maintenance-action {
|
||||||
|
min-height: 42px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: $color-bg;
|
||||||
|
padding: 0 16px;
|
||||||
|
color: $color-text-strong;
|
||||||
|
font-size: $font-size-14;
|
||||||
|
font-weight: 600;
|
||||||
|
white-space: nowrap;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: 0.18s ease;
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
background: $color-surface-hover;
|
||||||
|
//color: $color-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-primary {
|
||||||
|
background: $color-primary;
|
||||||
|
color: $color-text-inverse;
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
background: $color-primary-hover;
|
||||||
|
color: $color-text-inverse;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-maintenance-action--history {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 760px) {
|
||||||
|
.device-maintenance-summary {
|
||||||
|
grid-template-columns: 44px minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-maintenance-summary__comment {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
padding: 12px 0 0;
|
||||||
|
border-top: 1px solid $color-border-muted;
|
||||||
|
border-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-maintenance-options {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-maintenance-modal__footer {
|
||||||
|
align-items: stretch;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-maintenance-modal__actions,
|
||||||
|
.device-maintenance-action--history {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-maintenance-action {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,525 @@
|
||||||
|
import {
|
||||||
|
useEffect,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
type ReactNode,
|
||||||
|
} from 'react'
|
||||||
|
import * as Dialog from '@radix-ui/react-dialog'
|
||||||
|
import { useMutation, useQuery } from '@apollo/client/react'
|
||||||
|
import SimpleBar from 'simplebar-react'
|
||||||
|
import { Check, CircleAlert, History, Wrench, X } from 'lucide-react'
|
||||||
|
|
||||||
|
import {
|
||||||
|
GET_ACTIVE_PHONE_MAINTENANCE_CASE_QUERY,
|
||||||
|
MARK_PHONE_HEALTHY_MUTATION,
|
||||||
|
RETURN_PHONE_FROM_MAINTENANCE_MUTATION,
|
||||||
|
SEND_PHONE_TO_MAINTENANCE_MUTATION,
|
||||||
|
SET_PHONE_MAINTENANCE_REQUIRED_MUTATION,
|
||||||
|
} from '../../../../entities/device/api/device.graphql'
|
||||||
|
import {
|
||||||
|
deviceMalfunctionLabels,
|
||||||
|
isRedundantMalfunctionComment,
|
||||||
|
} from '../../../../entities/device/lib/maintenance'
|
||||||
|
import type {
|
||||||
|
DeviceMalfunction,
|
||||||
|
GetActivePhoneMaintenanceCaseData,
|
||||||
|
GetActivePhoneMaintenanceCaseVariables,
|
||||||
|
MarkPhoneHealthyData,
|
||||||
|
MarkPhoneHealthyVariables,
|
||||||
|
ReturnPhoneFromMaintenanceData,
|
||||||
|
ReturnPhoneFromMaintenanceVariables,
|
||||||
|
SendPhoneToMaintenanceData,
|
||||||
|
SendPhoneToMaintenanceVariables,
|
||||||
|
SetPhoneMaintenanceRequiredData,
|
||||||
|
SetPhoneMaintenanceRequiredVariables,
|
||||||
|
} from '../../../../entities/device/model/types'
|
||||||
|
import { MotionDialogContent } from '../../../../shared/ui/MotionDialog/MotionDialog'
|
||||||
|
import {
|
||||||
|
Notification,
|
||||||
|
type NotificationVariant,
|
||||||
|
} from '../../../../widgets/Notification/Notification'
|
||||||
|
import type { Device } from '../../types'
|
||||||
|
import { conditionText, getStatusClass } from '../../types'
|
||||||
|
import { DeviceMaintenanceMetrics } from '../DeviceMaintenanceMetrics/DeviceMaintenanceMetrics'
|
||||||
|
|
||||||
|
import './DeviceMaintenanceModal.scss'
|
||||||
|
|
||||||
|
export type DeviceMaintenanceMode = 'view' | 'manage'
|
||||||
|
|
||||||
|
type DeviceMaintenanceModalProps = {
|
||||||
|
open: boolean
|
||||||
|
mode: DeviceMaintenanceMode
|
||||||
|
device: Device
|
||||||
|
onOpenChange: (open: boolean) => void
|
||||||
|
onOpenHistory: () => void
|
||||||
|
onUpdated: () => Promise<unknown> | unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
type NotificationState = {
|
||||||
|
variant: NotificationVariant
|
||||||
|
title: string
|
||||||
|
description?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const malfunctionOptions: Array<{
|
||||||
|
value: DeviceMalfunction
|
||||||
|
description: string
|
||||||
|
}> = [
|
||||||
|
{
|
||||||
|
value: 'LowBatteryCapacity',
|
||||||
|
description: 'Остаточная ёмкость аккумулятора ниже допустимой.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'CriticalHits',
|
||||||
|
description: 'Количество зафиксированных ударов достигло лимита.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'CriticalChargeCycles',
|
||||||
|
description: 'Количество циклов зарядки достигло лимита.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'DoesNotTurnOn',
|
||||||
|
description: 'Устройство не загружается или не реагирует на включение.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'Other',
|
||||||
|
description: 'Неисправность не относится к перечисленным категориям.',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
function getErrorMessage(error: unknown) {
|
||||||
|
return error instanceof Error ? error.message : 'Не удалось выполнить действие.'
|
||||||
|
}
|
||||||
|
|
||||||
|
function ActionButton({
|
||||||
|
children,
|
||||||
|
className = '',
|
||||||
|
disabled,
|
||||||
|
onClick,
|
||||||
|
}: {
|
||||||
|
children: ReactNode
|
||||||
|
className?: string
|
||||||
|
disabled?: boolean
|
||||||
|
onClick: () => void
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className={`device-maintenance-action ${className}`}
|
||||||
|
type="button"
|
||||||
|
disabled={disabled}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DeviceMaintenanceModal({
|
||||||
|
open,
|
||||||
|
mode,
|
||||||
|
device,
|
||||||
|
onOpenChange,
|
||||||
|
onOpenHistory,
|
||||||
|
onUpdated,
|
||||||
|
}: DeviceMaintenanceModalProps) {
|
||||||
|
const [selectedMalfunctions, setSelectedMalfunctions] = useState<DeviceMalfunction[]>([])
|
||||||
|
const [comment, setComment] = useState('')
|
||||||
|
const [notification, setNotification] = useState<NotificationState | null>(null)
|
||||||
|
const [activeAction, setActiveAction] = useState<string | null>(null)
|
||||||
|
const initializedDraftKey = useRef('')
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: activeCaseData,
|
||||||
|
loading: isActiveCaseLoading,
|
||||||
|
error: activeCaseError,
|
||||||
|
refetch: refetchActiveCase,
|
||||||
|
} = useQuery<
|
||||||
|
GetActivePhoneMaintenanceCaseData,
|
||||||
|
GetActivePhoneMaintenanceCaseVariables
|
||||||
|
>(GET_ACTIVE_PHONE_MAINTENANCE_CASE_QUERY, {
|
||||||
|
variables: { phoneId: String(device.id) },
|
||||||
|
skip: !open,
|
||||||
|
fetchPolicy: 'cache-and-network',
|
||||||
|
})
|
||||||
|
|
||||||
|
const [setMaintenanceRequired] = useMutation<
|
||||||
|
SetPhoneMaintenanceRequiredData,
|
||||||
|
SetPhoneMaintenanceRequiredVariables
|
||||||
|
>(SET_PHONE_MAINTENANCE_REQUIRED_MUTATION)
|
||||||
|
const [markPhoneHealthy] = useMutation<
|
||||||
|
MarkPhoneHealthyData,
|
||||||
|
MarkPhoneHealthyVariables
|
||||||
|
>(MARK_PHONE_HEALTHY_MUTATION)
|
||||||
|
const [sendPhoneToMaintenance] = useMutation<
|
||||||
|
SendPhoneToMaintenanceData,
|
||||||
|
SendPhoneToMaintenanceVariables
|
||||||
|
>(SEND_PHONE_TO_MAINTENANCE_MUTATION)
|
||||||
|
const [returnPhoneFromMaintenance] = useMutation<
|
||||||
|
ReturnPhoneFromMaintenanceData,
|
||||||
|
ReturnPhoneFromMaintenanceVariables
|
||||||
|
>(RETURN_PHONE_FROM_MAINTENANCE_MUTATION)
|
||||||
|
|
||||||
|
const isManageMode = mode === 'manage'
|
||||||
|
const isBusy = activeAction !== null
|
||||||
|
const isOtherSelected = selectedMalfunctions.includes('Other')
|
||||||
|
const isDraftValid =
|
||||||
|
selectedMalfunctions.length > 0 && (!isOtherSelected || comment.trim().length > 0)
|
||||||
|
const visibleComment = isRedundantMalfunctionComment(
|
||||||
|
device.malfunctions,
|
||||||
|
device.malfunctionComment,
|
||||||
|
)
|
||||||
|
? ''
|
||||||
|
: device.malfunctionComment?.trim() ?? ''
|
||||||
|
const activeMaintenanceCase =
|
||||||
|
activeCaseData?.getActivePhoneMaintenanceCase ?? null
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) {
|
||||||
|
initializedDraftKey.current = ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const draftKey = `${device.id}:${device.technicalStatus}`
|
||||||
|
if (initializedDraftKey.current === draftKey) return
|
||||||
|
|
||||||
|
initializedDraftKey.current = draftKey
|
||||||
|
setSelectedMalfunctions(device.malfunctions)
|
||||||
|
setComment(device.malfunctionComment ?? '')
|
||||||
|
}, [
|
||||||
|
device.id,
|
||||||
|
device.malfunctionComment,
|
||||||
|
device.malfunctions,
|
||||||
|
device.technicalStatus,
|
||||||
|
open,
|
||||||
|
])
|
||||||
|
|
||||||
|
function toggleMalfunction(value: DeviceMalfunction) {
|
||||||
|
setSelectedMalfunctions((current) =>
|
||||||
|
current.includes(value)
|
||||||
|
? current.filter((item) => item !== value)
|
||||||
|
: [...current, value],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runAction(
|
||||||
|
action: string,
|
||||||
|
title: string,
|
||||||
|
callback: () => Promise<unknown>,
|
||||||
|
) {
|
||||||
|
setActiveAction(action)
|
||||||
|
|
||||||
|
try {
|
||||||
|
await callback()
|
||||||
|
await Promise.all([
|
||||||
|
Promise.resolve(onUpdated()),
|
||||||
|
refetchActiveCase(),
|
||||||
|
])
|
||||||
|
setNotification({ variant: 'success', title })
|
||||||
|
} catch (actionError) {
|
||||||
|
setNotification({
|
||||||
|
variant: 'error',
|
||||||
|
title: 'Не удалось изменить состояние устройства',
|
||||||
|
description: getErrorMessage(actionError),
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
setActiveAction(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveMaintenanceRequired() {
|
||||||
|
if (!isDraftValid) return
|
||||||
|
|
||||||
|
void runAction('save', 'Неисправность сохранена', () =>
|
||||||
|
setMaintenanceRequired({
|
||||||
|
variables: {
|
||||||
|
id: String(device.id),
|
||||||
|
malfunctions: selectedMalfunctions,
|
||||||
|
comment: comment.trim() || null,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function setHealthy() {
|
||||||
|
void runAction('healthy', 'Устройство признано исправным', () =>
|
||||||
|
markPhoneHealthy({ variables: { id: String(device.id) } }),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendToMaintenance() {
|
||||||
|
if (!isDraftValid) return
|
||||||
|
|
||||||
|
void runAction('service', 'Устройство отправлено на обслуживание', () =>
|
||||||
|
sendPhoneToMaintenance({
|
||||||
|
variables: {
|
||||||
|
id: String(device.id),
|
||||||
|
malfunctions: selectedMalfunctions,
|
||||||
|
comment: comment.trim() || null,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function returnToService() {
|
||||||
|
void runAction('return', 'Устройство возвращено в эксплуатацию', () =>
|
||||||
|
returnPhoneFromMaintenance({ variables: { id: String(device.id) } }),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Dialog.Root open={open} onOpenChange={onOpenChange}>
|
||||||
|
<MotionDialogContent
|
||||||
|
open={open}
|
||||||
|
overlayClassName="device-maintenance-modal__overlay"
|
||||||
|
contentClassName="device-maintenance-modal"
|
||||||
|
motionPreset="fade"
|
||||||
|
>
|
||||||
|
<header className="device-maintenance-modal__header">
|
||||||
|
<div>
|
||||||
|
<Dialog.Title>Техническое состояние</Dialog.Title>
|
||||||
|
<Dialog.Description>
|
||||||
|
{device.factoryNumber} · ID {device.id}
|
||||||
|
</Dialog.Description>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Dialog.Close asChild>
|
||||||
|
<button
|
||||||
|
className="device-maintenance-modal__close"
|
||||||
|
type="button"
|
||||||
|
aria-label="Закрыть"
|
||||||
|
disabled={isBusy}
|
||||||
|
>
|
||||||
|
<X size={20} />
|
||||||
|
</button>
|
||||||
|
</Dialog.Close>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<SimpleBar className="device-maintenance-modal__scroll">
|
||||||
|
<div className="device-maintenance-modal__body">
|
||||||
|
<section className="device-maintenance-summary">
|
||||||
|
<div className={`device-maintenance-summary__icon ${getStatusClass(device.condition)}`}>
|
||||||
|
{device.condition === 'ok' ? <Check size={22} /> : <Wrench size={22} />}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="device-maintenance-summary__field">
|
||||||
|
<span>Текущее состояние</span>
|
||||||
|
<strong>{conditionText[device.condition]}</strong>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{visibleComment && (
|
||||||
|
<div className="device-maintenance-summary__field device-maintenance-summary__comment">
|
||||||
|
<span>Комментарий</span>
|
||||||
|
<p>{visibleComment}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{device.technicalStatus !== 'Healthy' && (
|
||||||
|
<section className="device-maintenance-section">
|
||||||
|
<div className="device-maintenance-section__heading">
|
||||||
|
<div>
|
||||||
|
<h3>
|
||||||
|
{device.technicalStatus === 'InService'
|
||||||
|
? 'Показатели при отправке'
|
||||||
|
: 'Показатели устройства'}
|
||||||
|
</h3>
|
||||||
|
<p>
|
||||||
|
{device.technicalStatus === 'InService'
|
||||||
|
? 'Снимок устройства при передаче на обслуживание.'
|
||||||
|
: 'Текущие показатели для открытого случая.'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{activeMaintenanceCase ? (
|
||||||
|
<DeviceMaintenanceMetrics
|
||||||
|
metrics={activeMaintenanceCase.metrics}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<p className={`device-maintenance-metrics-message ${activeCaseError ? 'is-error' : ''}`}>
|
||||||
|
{isActiveCaseLoading
|
||||||
|
? 'Загружаем показатели...'
|
||||||
|
: activeCaseError
|
||||||
|
? 'Не удалось загрузить показатели.'
|
||||||
|
: 'Показатели не зафиксированы.'}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isManageMode && device.malfunctions.length > 0 && (
|
||||||
|
<section className="device-maintenance-section">
|
||||||
|
<div className="device-maintenance-section__heading">
|
||||||
|
<div>
|
||||||
|
<h3>Неисправности</h3>
|
||||||
|
<p>Неисправности, закреплённые за текущим состоянием.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="device-maintenance-current-malfunctions">
|
||||||
|
{device.malfunctions.map((malfunction) => (
|
||||||
|
<span key={malfunction}>
|
||||||
|
{deviceMalfunctionLabels[malfunction]}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isManageMode && device.technicalStatus !== 'InService' && (
|
||||||
|
<section className="device-maintenance-section">
|
||||||
|
<div className="device-maintenance-section__heading">
|
||||||
|
<div>
|
||||||
|
<h3>Неисправности</h3>
|
||||||
|
<p>Укажите одну или несколько обнаруженных неисправностей.</p>
|
||||||
|
</div>
|
||||||
|
<CircleAlert size={19} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="device-maintenance-options">
|
||||||
|
{malfunctionOptions.map((option) => {
|
||||||
|
const isSelected = selectedMalfunctions.includes(option.value)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className={`device-maintenance-option ${isSelected ? 'is-selected' : ''}`}
|
||||||
|
type="button"
|
||||||
|
key={option.value}
|
||||||
|
aria-pressed={isSelected}
|
||||||
|
disabled={isBusy}
|
||||||
|
onClick={() => toggleMalfunction(option.value)}
|
||||||
|
>
|
||||||
|
<span className="device-maintenance-option__check">
|
||||||
|
{isSelected && <Check size={15} />}
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<b>{deviceMalfunctionLabels[option.value]}</b>
|
||||||
|
<small>{option.description}</small>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label className="device-maintenance-comment">
|
||||||
|
<span>Комментарий {isOtherSelected && <b>обязателен</b>}</span>
|
||||||
|
<textarea
|
||||||
|
value={comment}
|
||||||
|
disabled={isBusy}
|
||||||
|
placeholder="Опишите неисправность или добавьте уточнение"
|
||||||
|
onChange={(event) => setComment(event.target.value)}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isManageMode &&
|
||||||
|
device.technicalStatus === 'InService' &&
|
||||||
|
device.malfunctions.length > 0 && (
|
||||||
|
<section className="device-maintenance-section">
|
||||||
|
<div className="device-maintenance-section__heading">
|
||||||
|
<div>
|
||||||
|
<h3>Неисправности</h3>
|
||||||
|
<p>Зафиксированы при отправке устройства на обслуживание.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="device-maintenance-current-malfunctions">
|
||||||
|
{device.malfunctions.map((malfunction) => (
|
||||||
|
<span key={malfunction}>
|
||||||
|
{deviceMalfunctionLabels[malfunction]}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</SimpleBar>
|
||||||
|
|
||||||
|
<footer className="device-maintenance-modal__footer">
|
||||||
|
<button
|
||||||
|
className="device-maintenance-action device-maintenance-action--history"
|
||||||
|
type="button"
|
||||||
|
disabled={isBusy}
|
||||||
|
onClick={onOpenHistory}
|
||||||
|
>
|
||||||
|
<History size={17} />
|
||||||
|
Журнал обслуживания
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="device-maintenance-modal__actions">
|
||||||
|
{isManageMode ? (
|
||||||
|
<>
|
||||||
|
{device.technicalStatus === 'Healthy' && (
|
||||||
|
<>
|
||||||
|
<ActionButton
|
||||||
|
disabled={!isDraftValid || isBusy}
|
||||||
|
onClick={saveMaintenanceRequired}
|
||||||
|
>
|
||||||
|
{activeAction === 'save' ? 'Сохраняем...' : 'Зафиксировать неисправность'}
|
||||||
|
</ActionButton>
|
||||||
|
<ActionButton
|
||||||
|
className="is-primary"
|
||||||
|
disabled={!isDraftValid || isBusy}
|
||||||
|
onClick={sendToMaintenance}
|
||||||
|
>
|
||||||
|
{activeAction === 'service' ? 'Отправляем...' : 'Отправить на обслуживание'}
|
||||||
|
</ActionButton>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{device.technicalStatus === 'NeedsMaintenance' && (
|
||||||
|
<>
|
||||||
|
<ActionButton disabled={isBusy} onClick={setHealthy}>
|
||||||
|
{activeAction === 'healthy' ? 'Сохраняем...' : 'Признать исправным'}
|
||||||
|
</ActionButton>
|
||||||
|
<ActionButton
|
||||||
|
disabled={!isDraftValid || isBusy}
|
||||||
|
onClick={saveMaintenanceRequired}
|
||||||
|
>
|
||||||
|
{activeAction === 'save' ? 'Сохраняем...' : 'Сохранить'}
|
||||||
|
</ActionButton>
|
||||||
|
<ActionButton
|
||||||
|
className="is-primary"
|
||||||
|
disabled={!isDraftValid || isBusy}
|
||||||
|
onClick={sendToMaintenance}
|
||||||
|
>
|
||||||
|
{activeAction === 'service' ? 'Отправляем...' : 'Отправить на обслуживание'}
|
||||||
|
</ActionButton>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{device.technicalStatus === 'InService' && (
|
||||||
|
<ActionButton
|
||||||
|
className="is-primary"
|
||||||
|
disabled={isBusy}
|
||||||
|
onClick={returnToService}
|
||||||
|
>
|
||||||
|
{activeAction === 'return' ? 'Возвращаем...' : 'Вернуть в эксплуатацию'}
|
||||||
|
</ActionButton>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Dialog.Close asChild>
|
||||||
|
<button className="device-maintenance-action" type="button">
|
||||||
|
Закрыть
|
||||||
|
</button>
|
||||||
|
</Dialog.Close>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</MotionDialogContent>
|
||||||
|
</Dialog.Root>
|
||||||
|
|
||||||
|
{notification && (
|
||||||
|
<Notification
|
||||||
|
variant={notification.variant}
|
||||||
|
title={notification.title}
|
||||||
|
description={notification.description}
|
||||||
|
onClose={() => setNotification(null)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -19,6 +19,7 @@ import type {
|
||||||
GetPhoneGpsTrackData,
|
GetPhoneGpsTrackData,
|
||||||
GetPhoneGpsTrackVariables,
|
GetPhoneGpsTrackVariables,
|
||||||
} from '../../../../entities/device/model/types'
|
} from '../../../../entities/device/model/types'
|
||||||
|
import { isPresent } from '../../../../shared/lib/isPresent'
|
||||||
import { FullscreenControl } from '../../../../widgets/FullscreenControlLeaflet/FullscreenControl'
|
import { FullscreenControl } from '../../../../widgets/FullscreenControlLeaflet/FullscreenControl'
|
||||||
import {
|
import {
|
||||||
MapTrackPeriodControl,
|
MapTrackPeriodControl,
|
||||||
|
|
@ -249,7 +250,9 @@ export function DeviceMapCard({
|
||||||
})
|
})
|
||||||
|
|
||||||
const sortedGpsTrack = useMemo(() => {
|
const sortedGpsTrack = useMemo(() => {
|
||||||
return sortGpsTrack(gpsTrackData?.getPhoneGpsTrack ?? [])
|
return sortGpsTrack(
|
||||||
|
(gpsTrackData?.getPhoneGpsTrack ?? []).filter(isPresent),
|
||||||
|
)
|
||||||
}, [gpsTrackData])
|
}, [gpsTrackData])
|
||||||
|
|
||||||
const telemetry = useMemo(() => {
|
const telemetry = useMemo(() => {
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
import type {
|
import type {
|
||||||
|
DeviceMalfunction,
|
||||||
DevicePolicy,
|
DevicePolicy,
|
||||||
DeviceTelemetryItem,
|
DeviceTelemetryItem,
|
||||||
|
DeviceTechnicalStatus,
|
||||||
GroupUsePolicy,
|
GroupUsePolicy,
|
||||||
} from '../../entities/device/model/types'
|
} from '../../entities/device/model/types'
|
||||||
|
|
||||||
export type DeviceCondition = 'ok' | 'inspection'
|
export type DeviceCondition = 'ok' | 'inspection' | 'service'
|
||||||
export type DeviceConnection = 'online' | 'offline' | 'offlineDanger'
|
export type DeviceConnection = 'online' | 'offline' | 'offlineDanger'
|
||||||
|
|
||||||
export type Device = {
|
export type Device = {
|
||||||
|
|
@ -21,6 +23,9 @@ export type Device = {
|
||||||
workTime: string | null
|
workTime: string | null
|
||||||
employee: string | null
|
employee: string | null
|
||||||
condition: DeviceCondition
|
condition: DeviceCondition
|
||||||
|
technicalStatus: DeviceTechnicalStatus
|
||||||
|
malfunctions: DeviceMalfunction[]
|
||||||
|
malfunctionComment?: string | null
|
||||||
connection: DeviceConnection
|
connection: DeviceConnection
|
||||||
connectionText: string
|
connectionText: string
|
||||||
registeredAt?: string
|
registeredAt?: string
|
||||||
|
|
@ -67,6 +72,7 @@ export type Device = {
|
||||||
export const conditionText: Record<DeviceCondition, string> = {
|
export const conditionText: Record<DeviceCondition, string> = {
|
||||||
ok: 'Исправно',
|
ok: 'Исправно',
|
||||||
inspection: 'Требует ТО',
|
inspection: 'Требует ТО',
|
||||||
|
service: 'На обслуживании',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const connectionText: Record<DeviceConnection, string> = {
|
export const connectionText: Record<DeviceConnection, string> = {
|
||||||
|
|
@ -78,6 +84,7 @@ export const connectionText: Record<DeviceConnection, string> = {
|
||||||
export function getStatusClass(status: DeviceCondition | DeviceConnection) {
|
export function getStatusClass(status: DeviceCondition | DeviceConnection) {
|
||||||
if (status === 'ok' || status === 'online') return 'is-success'
|
if (status === 'ok' || status === 'online') return 'is-success'
|
||||||
if (status === 'inspection' || status === 'offlineDanger') return 'is-danger'
|
if (status === 'inspection' || status === 'offlineDanger') return 'is-danger'
|
||||||
|
if (status === 'service') return 'is-service'
|
||||||
|
|
||||||
return 'is-muted'
|
return 'is-muted'
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -387,6 +387,11 @@
|
||||||
background-color: $color-danger-status-bg;
|
background-color: $color-danger-status-bg;
|
||||||
color: $red;
|
color: $red;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&--blue {
|
||||||
|
background-color: $color-primary-muted;
|
||||||
|
color: $blue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.devices-dot {
|
.devices-dot {
|
||||||
|
|
@ -408,6 +413,10 @@
|
||||||
background: $gray50;
|
background: $gray50;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.devices-dot--blue {
|
||||||
|
background: $blue;
|
||||||
|
}
|
||||||
|
|
||||||
.device-icons {
|
.device-icons {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(4, 20px);
|
grid-template-columns: repeat(4, 20px);
|
||||||
|
|
@ -514,7 +523,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.device-icons svg:not(.is-active):not(.is-danger) {
|
.device-icons svg:not(.is-active):not(.is-danger) {
|
||||||
background-color: $color-surface-active;
|
background-color: rgba($gray50-rgb, .15);
|
||||||
}
|
}
|
||||||
|
|
||||||
.devices-map-btn:hover {
|
.devices-map-btn:hover {
|
||||||
|
|
|
||||||
|
|
@ -42,9 +42,14 @@ import { GET_PHONES_QUERY } from '../../entities/device/api/device.graphql'
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
DeviceNetworkStatus,
|
DeviceNetworkStatus,
|
||||||
|
DeviceTechnicalStatus,
|
||||||
GetPhonesData,
|
GetPhonesData,
|
||||||
GetPhonesVariables,
|
GetPhonesVariables,
|
||||||
} from '../../entities/device/model/types'
|
} from '../../entities/device/model/types'
|
||||||
|
import {
|
||||||
|
getDeviceMalfunctionSummary,
|
||||||
|
deviceTechnicalStatusLabels,
|
||||||
|
} from '../../entities/device/lib/maintenance'
|
||||||
|
|
||||||
import type { Device as PageDevice } from '../DevicePage/types'
|
import type { Device as PageDevice } from '../DevicePage/types'
|
||||||
import type { Device as ApiDevice } from '../../entities/device/model/types'
|
import type { Device as ApiDevice } from '../../entities/device/model/types'
|
||||||
|
|
@ -58,6 +63,7 @@ import {
|
||||||
import { RevealContent } from '../../shared/ui/RevealContent/RevealContent'
|
import { RevealContent } from '../../shared/ui/RevealContent/RevealContent'
|
||||||
import { isPhonePolicyOptionEnabled } from '../../entities/device/lib/phonePolicy'
|
import { isPhonePolicyOptionEnabled } from '../../entities/device/lib/phonePolicy'
|
||||||
import { EmptyState } from '../../shared/ui/EmptyState/EmptyState'
|
import { EmptyState } from '../../shared/ui/EmptyState/EmptyState'
|
||||||
|
import { Tooltip } from '../../shared/ui/Tooltip/Tooltip'
|
||||||
import {
|
import {
|
||||||
preloadOnIdle,
|
preloadOnIdle,
|
||||||
useLazyMount,
|
useLazyMount,
|
||||||
|
|
@ -95,6 +101,12 @@ const DEVICE_NETWORK_STATUSES: DeviceNetworkStatus[] = [
|
||||||
'Lost',
|
'Lost',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const DEVICE_TECHNICAL_STATUSES: DeviceTechnicalStatus[] = [
|
||||||
|
'Healthy',
|
||||||
|
'NeedsMaintenance',
|
||||||
|
'InService',
|
||||||
|
]
|
||||||
|
|
||||||
const FILTERS_DRAWER_MEDIA_QUERY = '(max-width: 1600px)'
|
const FILTERS_DRAWER_MEDIA_QUERY = '(max-width: 1600px)'
|
||||||
|
|
||||||
function getIsFiltersDrawerViewport() {
|
function getIsFiltersDrawerViewport() {
|
||||||
|
|
@ -109,6 +121,12 @@ function isDeviceNetworkStatus(
|
||||||
return DEVICE_NETWORK_STATUSES.includes(value as DeviceNetworkStatus)
|
return DEVICE_NETWORK_STATUSES.includes(value as DeviceNetworkStatus)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isDeviceTechnicalStatus(
|
||||||
|
value: string,
|
||||||
|
): value is DeviceTechnicalStatus {
|
||||||
|
return DEVICE_TECHNICAL_STATUSES.includes(value as DeviceTechnicalStatus)
|
||||||
|
}
|
||||||
|
|
||||||
function formatDateTime(timestamp: number) {
|
function formatDateTime(timestamp: number) {
|
||||||
if (!timestamp) return 'Нет данных'
|
if (!timestamp) return 'Нет данных'
|
||||||
|
|
||||||
|
|
@ -121,16 +139,22 @@ function formatDateTime(timestamp: number) {
|
||||||
}).format(new Date(timestamp))
|
}).format(new Date(timestamp))
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDeviceConditionLabel(needMaintenance?: boolean) {
|
function getDeviceConditionLabel(status?: DeviceTechnicalStatus) {
|
||||||
return needMaintenance ? 'Требует ТО' : 'Исправно'
|
return deviceTechnicalStatusLabels[status ?? 'Healthy']
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDeviceConditionClass(needMaintenance?: boolean) {
|
function getDeviceConditionClass(status?: DeviceTechnicalStatus) {
|
||||||
return needMaintenance ? 'devices-status--red' : 'devices-status--green'
|
if (status === 'NeedsMaintenance') return 'devices-status--red'
|
||||||
|
if (status === 'InService') return 'devices-status--blue'
|
||||||
|
|
||||||
|
return 'devices-status--green'
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDeviceConditionDotClass(needMaintenance?: boolean) {
|
function getDeviceConditionDotClass(status?: DeviceTechnicalStatus) {
|
||||||
return needMaintenance ? 'devices-dot--red' : 'devices-dot--green'
|
if (status === 'NeedsMaintenance') return 'devices-dot--red'
|
||||||
|
if (status === 'InService') return 'devices-dot--blue'
|
||||||
|
|
||||||
|
return 'devices-dot--green'
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDeviceNetworkLabel(status?: DeviceNetworkStatus) {
|
function getDeviceNetworkLabel(status?: DeviceNetworkStatus) {
|
||||||
|
|
@ -215,19 +239,26 @@ const DevicesTableRow = memo(function DevicesTableRow({
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td>
|
<td>
|
||||||
<div
|
<Tooltip
|
||||||
className={`devices-status ${getDeviceConditionClass(
|
content={getDeviceMalfunctionSummary(
|
||||||
device.techState?.needMaintenance,
|
device.techState?.malfunctions,
|
||||||
)}`}
|
device.techState?.malfunctionComment,
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className={`devices-dot ${getDeviceConditionDotClass(
|
className={`devices-status ${getDeviceConditionClass(
|
||||||
device.techState?.needMaintenance,
|
device.techState?.status,
|
||||||
)}`}
|
)}`}
|
||||||
/>
|
>
|
||||||
|
<span
|
||||||
|
className={`devices-dot ${getDeviceConditionDotClass(
|
||||||
|
device.techState?.status,
|
||||||
|
)}`}
|
||||||
|
/>
|
||||||
|
|
||||||
{getDeviceConditionLabel(device.techState?.needMaintenance)}
|
{getDeviceConditionLabel(device.techState?.status)}
|
||||||
</div>
|
</span>
|
||||||
|
</Tooltip>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td>
|
<td>
|
||||||
|
|
@ -396,11 +427,9 @@ export function DevicesPage() {
|
||||||
|
|
||||||
const selectedNetworkStatuses = useMemo(
|
const selectedNetworkStatuses = useMemo(
|
||||||
() =>
|
() =>
|
||||||
networkStatusParam === 'none'
|
networkStatusParam
|
||||||
? []
|
?.split(',')
|
||||||
: networkStatusParam
|
.filter(isDeviceNetworkStatus) ?? [],
|
||||||
?.split(',')
|
|
||||||
.filter(isDeviceNetworkStatus) ?? [],
|
|
||||||
[networkStatusParam],
|
[networkStatusParam],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -412,6 +441,15 @@ export function DevicesPage() {
|
||||||
? false
|
? false
|
||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
|
const technicalStatusParam = searchParams.get('dTechnicalStatus')
|
||||||
|
const selectedTechnicalStatuses = useMemo(
|
||||||
|
() =>
|
||||||
|
technicalStatusParam
|
||||||
|
?.split(',')
|
||||||
|
.filter(isDeviceTechnicalStatus) ?? [],
|
||||||
|
[technicalStatusParam],
|
||||||
|
)
|
||||||
|
|
||||||
const lockedParam = searchParams.get('dLocked')
|
const lockedParam = searchParams.get('dLocked')
|
||||||
const locked =
|
const locked =
|
||||||
lockedParam === 'true'
|
lockedParam === 'true'
|
||||||
|
|
@ -488,21 +526,26 @@ export function DevicesPage() {
|
||||||
: undefined,
|
: undefined,
|
||||||
locked,
|
locked,
|
||||||
networkStatus:
|
networkStatus:
|
||||||
networkStatusParam
|
selectedNetworkStatuses.length > 0
|
||||||
? selectedNetworkStatuses
|
? selectedNetworkStatuses
|
||||||
: undefined,
|
: undefined,
|
||||||
needMaintenance,
|
needMaintenance,
|
||||||
|
technicalStatus:
|
||||||
|
selectedTechnicalStatuses.length > 0
|
||||||
|
? selectedTechnicalStatuses
|
||||||
|
: undefined,
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
currentPage,
|
currentPage,
|
||||||
debouncedDeviceSearch,
|
debouncedDeviceSearch,
|
||||||
locked,
|
locked,
|
||||||
needMaintenance,
|
needMaintenance,
|
||||||
networkStatusParam,
|
selectedTechnicalStatuses,
|
||||||
selectedNetworkStatuses,
|
selectedNetworkStatuses,
|
||||||
selectedOrganisationIds,
|
selectedOrganisationIds,
|
||||||
sortDirection,
|
sortDirection,
|
||||||
sortField,
|
sortField,
|
||||||
|
technicalStatusParam,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -534,8 +577,9 @@ export function DevicesPage() {
|
||||||
const hasAppliedFilters = Boolean(
|
const hasAppliedFilters = Boolean(
|
||||||
debouncedDeviceSearch ||
|
debouncedDeviceSearch ||
|
||||||
selectedOrganisationIds.length > 0 ||
|
selectedOrganisationIds.length > 0 ||
|
||||||
networkStatusParam ||
|
selectedNetworkStatuses.length > 0 ||
|
||||||
needMaintenance !== undefined ||
|
needMaintenance !== undefined ||
|
||||||
|
selectedTechnicalStatuses.length > 0 ||
|
||||||
locked !== undefined,
|
locked !== undefined,
|
||||||
)
|
)
|
||||||
const shouldShowEmptyState = !loading && !error && !hasDevices
|
const shouldShowEmptyState = !loading && !error && !hasDevices
|
||||||
|
|
@ -628,8 +672,7 @@ export function DevicesPage() {
|
||||||
)
|
)
|
||||||
const isLocked = policy?.locked ?? false
|
const isLocked = policy?.locked ?? false
|
||||||
|
|
||||||
const needMaintenance =
|
const technicalStatus = device.techState?.status ?? 'Healthy'
|
||||||
device.techState?.needMaintenance ?? false
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: device.id,
|
id: device.id,
|
||||||
|
|
@ -647,9 +690,15 @@ export function DevicesPage() {
|
||||||
policy,
|
policy,
|
||||||
organisationPolicy: device.org?.policy ?? null,
|
organisationPolicy: device.org?.policy ?? null,
|
||||||
|
|
||||||
condition: needMaintenance
|
condition:
|
||||||
? 'inspection'
|
technicalStatus === 'InService'
|
||||||
: 'ok',
|
? 'service'
|
||||||
|
: technicalStatus === 'NeedsMaintenance'
|
||||||
|
? 'inspection'
|
||||||
|
: 'ok',
|
||||||
|
technicalStatus,
|
||||||
|
malfunctions: device.techState?.malfunctions ?? [],
|
||||||
|
malfunctionComment: device.techState?.malfunctionComment,
|
||||||
|
|
||||||
connection: getDeviceNetworkConnection(device.networkStatus),
|
connection: getDeviceNetworkConnection(device.networkStatus),
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,10 @@ import * as Accordion from '@radix-ui/react-accordion'
|
||||||
import { useSearchParams } from 'react-router-dom'
|
import { useSearchParams } from 'react-router-dom'
|
||||||
import { ChevronDown } from 'lucide-react'
|
import { ChevronDown } from 'lucide-react'
|
||||||
|
|
||||||
import type { DeviceNetworkStatus } from '../../../../entities/device/model/types'
|
import type {
|
||||||
|
DeviceNetworkStatus,
|
||||||
|
DeviceTechnicalStatus,
|
||||||
|
} from '../../../../entities/device/model/types'
|
||||||
import {
|
import {
|
||||||
DevicesDateRangePicker,
|
DevicesDateRangePicker,
|
||||||
type DevicesDateRangePickerValue,
|
type DevicesDateRangePickerValue,
|
||||||
|
|
@ -31,21 +34,32 @@ const networkStatusOptions: {
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const maintenanceOptions = [
|
const technicalStatusOptions: Array<{
|
||||||
|
value: DeviceTechnicalStatus
|
||||||
|
label: string
|
||||||
|
}> = [
|
||||||
{
|
{
|
||||||
value: false,
|
value: 'Healthy',
|
||||||
label: 'Исправно',
|
label: 'Исправно',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: true,
|
value: 'NeedsMaintenance',
|
||||||
label: 'Требует ТО',
|
label: 'Требует ТО',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
value: 'InService',
|
||||||
|
label: 'На обслуживании',
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
function isDeviceNetworkStatus(value: string): value is DeviceNetworkStatus {
|
function isDeviceNetworkStatus(value: string): value is DeviceNetworkStatus {
|
||||||
return networkStatusOptions.some((option) => option.value === value)
|
return networkStatusOptions.some((option) => option.value === value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isDeviceTechnicalStatus(value: string): value is DeviceTechnicalStatus {
|
||||||
|
return technicalStatusOptions.some((option) => option.value === value)
|
||||||
|
}
|
||||||
|
|
||||||
type DevicesFiltersPanelProps = {
|
type DevicesFiltersPanelProps = {
|
||||||
isOpen: boolean
|
isOpen: boolean
|
||||||
}
|
}
|
||||||
|
|
@ -63,24 +77,31 @@ export const DevicesFiltersPanel = memo(function DevicesFiltersPanel({
|
||||||
const networkStatusParam = searchParams.get('dNetwork')
|
const networkStatusParam = searchParams.get('dNetwork')
|
||||||
const selectedNetworkStatuses = useMemo<DeviceNetworkStatus[]>(
|
const selectedNetworkStatuses = useMemo<DeviceNetworkStatus[]>(
|
||||||
() =>
|
() =>
|
||||||
networkStatusParam === 'none'
|
networkStatusParam
|
||||||
? []
|
?.split(',')
|
||||||
: networkStatusParam
|
.filter(isDeviceNetworkStatus) ?? [],
|
||||||
?.split(',')
|
|
||||||
.filter(isDeviceNetworkStatus) ??
|
|
||||||
networkStatusOptions.map((option) => option.value),
|
|
||||||
[networkStatusParam],
|
[networkStatusParam],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const technicalStatusParam = searchParams.get('dTechnicalStatus')
|
||||||
const needMaintenanceParam = searchParams.get('dNeedMaintenance')
|
const needMaintenanceParam = searchParams.get('dNeedMaintenance')
|
||||||
const selectedMaintenanceStates = useMemo(
|
const selectedTechnicalStatuses = useMemo<DeviceTechnicalStatus[]>(
|
||||||
() =>
|
() => {
|
||||||
needMaintenanceParam === 'true'
|
if (technicalStatusParam) {
|
||||||
? [true]
|
return technicalStatusParam
|
||||||
: needMaintenanceParam === 'false'
|
.split(',')
|
||||||
? [false]
|
.filter(isDeviceTechnicalStatus)
|
||||||
: maintenanceOptions.map((option) => option.value),
|
}
|
||||||
[needMaintenanceParam],
|
|
||||||
|
if (needMaintenanceParam === 'true') {
|
||||||
|
return ['NeedsMaintenance', 'InService']
|
||||||
|
}
|
||||||
|
|
||||||
|
if (needMaintenanceParam === 'false') return ['Healthy']
|
||||||
|
|
||||||
|
return []
|
||||||
|
},
|
||||||
|
[needMaintenanceParam, technicalStatusParam],
|
||||||
)
|
)
|
||||||
|
|
||||||
const [workPeriod, setWorkPeriod] = useState<DevicesDateRangePickerValue>({
|
const [workPeriod, setWorkPeriod] = useState<DevicesDateRangePickerValue>({
|
||||||
|
|
@ -123,17 +144,18 @@ export const DevicesFiltersPanel = memo(function DevicesFiltersPanel({
|
||||||
|
|
||||||
updateSearchParams({
|
updateSearchParams({
|
||||||
dNetwork:
|
dNetwork:
|
||||||
nextStatuses.length === networkStatusOptions.length
|
nextStatuses.length > 0
|
||||||
? null
|
? nextStatuses.join(',')
|
||||||
: nextStatuses.length === 0
|
: null,
|
||||||
? 'none'
|
|
||||||
: nextStatuses.join(','),
|
|
||||||
dPage: '0',
|
dPage: '0',
|
||||||
})
|
})
|
||||||
}, [selectedNetworkStatuses, updateSearchParams])
|
}, [selectedNetworkStatuses, updateSearchParams])
|
||||||
|
|
||||||
const handleMaintenanceChange = useCallback((value: boolean, checked: boolean) => {
|
const handleTechnicalStatusChange = useCallback((
|
||||||
const currentStates = new Set(selectedMaintenanceStates)
|
value: DeviceTechnicalStatus,
|
||||||
|
checked: boolean,
|
||||||
|
) => {
|
||||||
|
const currentStates = new Set(selectedTechnicalStatuses)
|
||||||
|
|
||||||
if (checked) {
|
if (checked) {
|
||||||
currentStates.add(value)
|
currentStates.add(value)
|
||||||
|
|
@ -141,20 +163,19 @@ export const DevicesFiltersPanel = memo(function DevicesFiltersPanel({
|
||||||
currentStates.delete(value)
|
currentStates.delete(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentStates.size === 0) return
|
const nextStates = technicalStatusOptions
|
||||||
|
|
||||||
const nextStates = maintenanceOptions
|
|
||||||
.map((option) => option.value)
|
.map((option) => option.value)
|
||||||
.filter((optionValue) => currentStates.has(optionValue))
|
.filter((optionValue) => currentStates.has(optionValue))
|
||||||
|
|
||||||
updateSearchParams({
|
updateSearchParams({
|
||||||
dNeedMaintenance:
|
dTechnicalStatus:
|
||||||
nextStates.length === maintenanceOptions.length
|
nextStates.length > 0
|
||||||
? null
|
? nextStates.join(',')
|
||||||
: String(nextStates[0]),
|
: null,
|
||||||
|
dNeedMaintenance: null,
|
||||||
dPage: '0',
|
dPage: '0',
|
||||||
})
|
})
|
||||||
}, [selectedMaintenanceStates, updateSearchParams])
|
}, [selectedTechnicalStatuses, updateSearchParams])
|
||||||
|
|
||||||
const handleOrganisationsChange = useCallback((ids: string[]) => {
|
const handleOrganisationsChange = useCallback((ids: string[]) => {
|
||||||
updateSearchParams({
|
updateSearchParams({
|
||||||
|
|
@ -173,7 +194,7 @@ export const DevicesFiltersPanel = memo(function DevicesFiltersPanel({
|
||||||
type="multiple"
|
type="multiple"
|
||||||
defaultValue={['work-period']}
|
defaultValue={['work-period']}
|
||||||
>
|
>
|
||||||
<Accordion.Item className="devices-filter-item" value="work-period">
|
{/* <Accordion.Item className="devices-filter-item" value="work-period">
|
||||||
<Accordion.Header className="devices-filter-item__header">
|
<Accordion.Header className="devices-filter-item__header">
|
||||||
<Accordion.Trigger className="devices-filter-item__trigger">
|
<Accordion.Trigger className="devices-filter-item__trigger">
|
||||||
<span>Период работы</span>
|
<span>Период работы</span>
|
||||||
|
|
@ -194,7 +215,7 @@ export const DevicesFiltersPanel = memo(function DevicesFiltersPanel({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Accordion.Content>
|
</Accordion.Content>
|
||||||
</Accordion.Item>
|
</Accordion.Item> */}
|
||||||
|
|
||||||
<Accordion.Item
|
<Accordion.Item
|
||||||
className="devices-filter-item"
|
className="devices-filter-item"
|
||||||
|
|
@ -269,13 +290,13 @@ export const DevicesFiltersPanel = memo(function DevicesFiltersPanel({
|
||||||
<Accordion.Content className="devices-filter-item__content">
|
<Accordion.Content className="devices-filter-item__content">
|
||||||
<div className="devices-filter-item__inner">
|
<div className="devices-filter-item__inner">
|
||||||
<div className="devices-checkbox-list">
|
<div className="devices-checkbox-list">
|
||||||
{maintenanceOptions.map((option) => (
|
{technicalStatusOptions.map((option) => (
|
||||||
<label className="devices-checkbox" key={String(option.value)}>
|
<label className="devices-checkbox" key={option.value}>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={selectedMaintenanceStates.includes(option.value)}
|
checked={selectedTechnicalStatuses.includes(option.value)}
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
handleMaintenanceChange(
|
handleTechnicalStatusChange(
|
||||||
option.value,
|
option.value,
|
||||||
event.target.checked,
|
event.target.checked,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { useQuery } from '@apollo/client/react'
|
import { useQuery } from '@apollo/client/react'
|
||||||
import { memo, useCallback, useMemo } from 'react'
|
import { memo, useCallback, useEffect, useMemo } from 'react'
|
||||||
import { useSearchParams } from 'react-router-dom'
|
import { useSearchParams } from 'react-router-dom'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
|
@ -14,15 +14,18 @@ import {
|
||||||
type PageTabItem,
|
type PageTabItem,
|
||||||
} from '../../../../shared/ui/PageTabs/PageTabs'
|
} from '../../../../shared/ui/PageTabs/PageTabs'
|
||||||
|
|
||||||
type DevicesTab = 'working' | 'lost' | 'locked' | 'maintenance'
|
type DevicesTab = 'working' | 'lost' | 'locked' | 'maintenance' | 'service'
|
||||||
type DevicesTabsValue = DevicesTab | 'none'
|
type DevicesTabsValue = DevicesTab | 'none'
|
||||||
|
|
||||||
function getActiveTab(searchParams: URLSearchParams): DevicesTabsValue {
|
function getActiveTab(searchParams: URLSearchParams): DevicesTabsValue {
|
||||||
const locked = searchParams.get('dLocked')
|
const locked = searchParams.get('dLocked')
|
||||||
const needMaintenance = searchParams.get('dNeedMaintenance')
|
const needMaintenance = searchParams.get('dNeedMaintenance')
|
||||||
const networkStatus = searchParams.get('dNetwork')
|
const networkStatus = searchParams.get('dNetwork')
|
||||||
|
const technicalStatus = searchParams.get('dTechnicalStatus')
|
||||||
|
|
||||||
if (locked === 'true') return 'locked'
|
if (locked === 'true') return 'locked'
|
||||||
|
if (technicalStatus === 'NeedsMaintenance') return 'maintenance'
|
||||||
|
if (technicalStatus === 'InService') return 'service'
|
||||||
if (needMaintenance === 'true') return 'maintenance'
|
if (needMaintenance === 'true') return 'maintenance'
|
||||||
if (networkStatus === 'Lost') return 'lost'
|
if (networkStatus === 'Lost') return 'lost'
|
||||||
if (networkStatus === 'Online') return 'working'
|
if (networkStatus === 'Online') return 'working'
|
||||||
|
|
@ -40,6 +43,27 @@ export const DevicesTabs = memo(function DevicesTabs() {
|
||||||
const [searchParams, setSearchParams] = useSearchParams()
|
const [searchParams, setSearchParams] = useSearchParams()
|
||||||
const activeTab = useMemo(() => getActiveTab(searchParams), [searchParams])
|
const activeTab = useMemo(() => getActiveTab(searchParams), [searchParams])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const legacyNeedMaintenance = searchParams.get('dNeedMaintenance')
|
||||||
|
|
||||||
|
if (
|
||||||
|
searchParams.has('dTechnicalStatus') ||
|
||||||
|
(legacyNeedMaintenance !== 'true' && legacyNeedMaintenance !== 'false')
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextParams = new URLSearchParams(searchParams)
|
||||||
|
|
||||||
|
nextParams.delete('dNeedMaintenance')
|
||||||
|
nextParams.set(
|
||||||
|
'dTechnicalStatus',
|
||||||
|
legacyNeedMaintenance === 'true' ? 'NeedsMaintenance' : 'Healthy',
|
||||||
|
)
|
||||||
|
|
||||||
|
setSearchParams(nextParams, { replace: true })
|
||||||
|
}, [searchParams, setSearchParams])
|
||||||
|
|
||||||
const query = searchParams.get('dQuery')?.trim() || undefined
|
const query = searchParams.get('dQuery')?.trim() || undefined
|
||||||
const selectedOrganisationIds = useMemo(() => {
|
const selectedOrganisationIds = useMemo(() => {
|
||||||
return searchParams.get('dOrgs')?.split(',').filter(Boolean) ?? []
|
return searchParams.get('dOrgs')?.split(',').filter(Boolean) ?? []
|
||||||
|
|
@ -70,6 +94,7 @@ export const DevicesTabs = memo(function DevicesTabs() {
|
||||||
const lockedCount = stats?.lockedPhones.totalElements
|
const lockedCount = stats?.lockedPhones.totalElements
|
||||||
const lostCount = stats?.lostPhones.totalElements
|
const lostCount = stats?.lostPhones.totalElements
|
||||||
const maintenanceCount = stats?.maintenancePhones.totalElements
|
const maintenanceCount = stats?.maintenancePhones.totalElements
|
||||||
|
const serviceCount = stats?.servicePhones.totalElements
|
||||||
|
|
||||||
const tabs = useMemo<Array<PageTabItem<DevicesTabsValue>>>(() => {
|
const tabs = useMemo<Array<PageTabItem<DevicesTabsValue>>>(() => {
|
||||||
const nextTabs: Array<PageTabItem<DevicesTabsValue>> = [
|
const nextTabs: Array<PageTabItem<DevicesTabsValue>> = [
|
||||||
|
|
@ -119,8 +144,19 @@ export const DevicesTabs = memo(function DevicesTabs() {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ((serviceCount ?? 0) > 0) {
|
||||||
|
nextTabs.push({
|
||||||
|
value: 'service',
|
||||||
|
label: (
|
||||||
|
<>
|
||||||
|
На обслуживании: <b>{formatCount(serviceCount)}</b>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return nextTabs
|
return nextTabs
|
||||||
}, [lockedCount, lostCount, maintenanceCount, onlineCount, totalCount])
|
}, [lockedCount, lostCount, maintenanceCount, onlineCount, serviceCount, totalCount])
|
||||||
|
|
||||||
const applyQuickFilter = useCallback((tab: DevicesTabsValue) => {
|
const applyQuickFilter = useCallback((tab: DevicesTabsValue) => {
|
||||||
if (tab === 'none') return
|
if (tab === 'none') return
|
||||||
|
|
@ -133,6 +169,7 @@ export const DevicesTabs = memo(function DevicesTabs() {
|
||||||
nextParams.delete('dLocked')
|
nextParams.delete('dLocked')
|
||||||
nextParams.delete('dNetwork')
|
nextParams.delete('dNetwork')
|
||||||
nextParams.delete('dNeedMaintenance')
|
nextParams.delete('dNeedMaintenance')
|
||||||
|
nextParams.delete('dTechnicalStatus')
|
||||||
setSearchParams(nextParams, { replace: true })
|
setSearchParams(nextParams, { replace: true })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -141,24 +178,35 @@ export const DevicesTabs = memo(function DevicesTabs() {
|
||||||
nextParams.set('dNetwork', 'Online')
|
nextParams.set('dNetwork', 'Online')
|
||||||
nextParams.delete('dLocked')
|
nextParams.delete('dLocked')
|
||||||
nextParams.delete('dNeedMaintenance')
|
nextParams.delete('dNeedMaintenance')
|
||||||
|
nextParams.delete('dTechnicalStatus')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tab === 'locked') {
|
if (tab === 'locked') {
|
||||||
nextParams.set('dLocked', 'true')
|
nextParams.set('dLocked', 'true')
|
||||||
nextParams.delete('dNetwork')
|
nextParams.delete('dNetwork')
|
||||||
nextParams.delete('dNeedMaintenance')
|
nextParams.delete('dNeedMaintenance')
|
||||||
|
nextParams.delete('dTechnicalStatus')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tab === 'lost') {
|
if (tab === 'lost') {
|
||||||
nextParams.set('dNetwork', 'Lost')
|
nextParams.set('dNetwork', 'Lost')
|
||||||
nextParams.delete('dLocked')
|
nextParams.delete('dLocked')
|
||||||
nextParams.delete('dNeedMaintenance')
|
nextParams.delete('dNeedMaintenance')
|
||||||
|
nextParams.delete('dTechnicalStatus')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tab === 'maintenance') {
|
if (tab === 'maintenance') {
|
||||||
nextParams.delete('dLocked')
|
nextParams.delete('dLocked')
|
||||||
nextParams.delete('dNetwork')
|
nextParams.delete('dNetwork')
|
||||||
nextParams.set('dNeedMaintenance', 'true')
|
nextParams.delete('dNeedMaintenance')
|
||||||
|
nextParams.set('dTechnicalStatus', 'NeedsMaintenance')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tab === 'service') {
|
||||||
|
nextParams.delete('dLocked')
|
||||||
|
nextParams.delete('dNetwork')
|
||||||
|
nextParams.delete('dNeedMaintenance')
|
||||||
|
nextParams.set('dTechnicalStatus', 'InService')
|
||||||
}
|
}
|
||||||
|
|
||||||
setSearchParams(nextParams, { replace: true })
|
setSearchParams(nextParams, { replace: true })
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,12 @@
|
||||||
.add-organisation-modal--with-policy {
|
.add-organisation-modal--with-policy {
|
||||||
width: min(980px, calc(100vw - 32px));
|
width: min(980px, calc(100vw - 32px));
|
||||||
}
|
}
|
||||||
|
.add-organisation-modal--without-policy{
|
||||||
|
width: min(460px, calc(100vw - 32px));
|
||||||
|
.avatar-upload{
|
||||||
|
width: inherit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.add-organisation-modal__header {
|
.add-organisation-modal__header {
|
||||||
margin-bottom: 22px;
|
margin-bottom: 22px;
|
||||||
|
|
|
||||||
|
|
@ -104,7 +104,7 @@ export function AddOrganisationModal({
|
||||||
const modalClassName = useMemo(
|
const modalClassName = useMemo(
|
||||||
() =>
|
() =>
|
||||||
`add-organisation-modal ${
|
`add-organisation-modal ${
|
||||||
!isEditMode ? 'add-organisation-modal--with-policy' : ''
|
!isEditMode ? 'add-organisation-modal--with-policy' : 'add-organisation-modal--without-policy'
|
||||||
}`,
|
}`,
|
||||||
[isEditMode],
|
[isEditMode],
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -57,154 +57,60 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-theme-switch {
|
.login-theme-button {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 24px;
|
top: 24px;
|
||||||
right: 24px;
|
right: 24px;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
|
|
||||||
width: 72px;
|
width: 42px;
|
||||||
height: 32px;
|
height: 42px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 999px;
|
border-radius: 14px;
|
||||||
background: transparent;
|
background: $color-surface-glass;
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
box-shadow:
|
||||||
|
inset 0 0 1px 1px rgba($gray50-rgb, 0.18),
|
||||||
|
0 10px 28px rgba($color-shadow-rgb, 0.12);
|
||||||
|
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-theme-switch__track {
|
|
||||||
position: relative;
|
|
||||||
width: 62px;
|
|
||||||
height: 26px;
|
|
||||||
padding: 4px;
|
|
||||||
border-radius: 999px;
|
|
||||||
|
|
||||||
background: $color-surface-glass;
|
|
||||||
box-shadow:
|
|
||||||
inset 0 0 1px 1px rgba($gray50-rgb, 0.24),
|
|
||||||
0 10px 28px rgba($color-shadow-rgb, 0.12);
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
transition:
|
|
||||||
background 0.25s ease,
|
|
||||||
box-shadow 0.25s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-theme-switch__track::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
border-radius: inherit;
|
|
||||||
|
|
||||||
background:
|
|
||||||
radial-gradient(circle at 25% 35%, rgba($orange, 0.22), transparent 34%),
|
|
||||||
radial-gradient(circle at 78% 65%, rgba($blue, 0.2), transparent 38%);
|
|
||||||
|
|
||||||
opacity: 0.92;
|
|
||||||
transition: opacity 0.25s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-theme-switch__thumb {
|
|
||||||
position: relative;
|
|
||||||
z-index: 2;
|
|
||||||
|
|
||||||
width: 26px;
|
|
||||||
height: 26px;
|
|
||||||
border-radius: 50%;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
background: $white;
|
|
||||||
color: $orange;
|
color: $orange;
|
||||||
box-shadow: 0 1px 8px -2px rgba($gray50-rgb, 0.8);
|
cursor: pointer;
|
||||||
transform: translateX(0);
|
|
||||||
|
|
||||||
transition:
|
transition:
|
||||||
transform 0.28s cubic-bezier(0.34, 1.56, 0.64, 1),
|
transform 0.18s ease,
|
||||||
background 0.25s ease,
|
background 0.18s ease,
|
||||||
color 0.25s ease,
|
color 0.18s ease,
|
||||||
box-shadow 0.25s ease;
|
box-shadow 0.18s ease;
|
||||||
}
|
|
||||||
|
|
||||||
.login-theme-switch__icon {
|
svg {
|
||||||
position: absolute;
|
flex: 0 0 auto;
|
||||||
z-index: 1;
|
transition: transform 0.2s ease;
|
||||||
top: 50%;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
color: $color-text-muted;
|
|
||||||
opacity: 0.55;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
|
|
||||||
transition:
|
|
||||||
opacity 0.25s ease,
|
|
||||||
color 0.25s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-theme-switch__icon--sun {
|
|
||||||
left: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-theme-switch__icon--moon {
|
|
||||||
right: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-theme-switch.is-light {
|
|
||||||
.login-theme-switch__icon--sun {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-theme-switch__icon--moon {
|
&:hover {
|
||||||
opacity: 0.45;
|
background: $color-surface;
|
||||||
}
|
color: $blue;
|
||||||
}
|
transform: translateY(-1px);
|
||||||
|
|
||||||
.login-theme-switch.is-dark {
|
svg {
|
||||||
.login-theme-switch__track {
|
transform: rotate(-8deg) scale(1.05);
|
||||||
background: $color-surface-glass;
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: scale(0.96);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-dark {
|
||||||
|
color: $blue;
|
||||||
box-shadow:
|
box-shadow:
|
||||||
inset 0 0 0 1px rgba($white, 0.08),
|
inset 0 0 0 1px rgba($white, 0.08),
|
||||||
0 12px 30px rgba($black, 0.24);
|
0 12px 30px rgba($black, 0.24);
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-theme-switch__track::before {
|
|
||||||
opacity: 0.65;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-theme-switch__thumb {
|
|
||||||
transform: translateX(36px);
|
|
||||||
background: $color-surface;
|
|
||||||
color: $blue;
|
|
||||||
box-shadow: 0 1px 6px -2px $gray50;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-theme-switch__icon--sun {
|
|
||||||
opacity: 0.38;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-theme-switch__icon--moon {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-theme-switch:active {
|
|
||||||
.login-theme-switch__thumb {
|
|
||||||
transform: scale(0.94);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.is-dark .login-theme-switch__thumb {
|
|
||||||
transform: translateX(36px) scale(0.94);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-card {
|
.login-card {
|
||||||
|
|
@ -410,7 +316,7 @@
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-theme-switch {
|
.login-theme-button {
|
||||||
top: 16px;
|
top: 16px;
|
||||||
right: 16px;
|
right: 16px;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -54,25 +54,13 @@ export function LoginPage({ onSuccess }: LoginPageProps) {
|
||||||
return (
|
return (
|
||||||
<main className="login-page">
|
<main className="login-page">
|
||||||
<button
|
<button
|
||||||
className={`login-theme-switch ${isDark ? 'is-dark' : 'is-light'}`}
|
className={`login-theme-button ${isDark ? 'is-dark' : 'is-light'}`}
|
||||||
type="button"
|
type="button"
|
||||||
aria-label={isDark ? 'Включить светлую тему' : 'Включить тёмную тему'}
|
aria-label={isDark ? 'Включить светлую тему' : 'Включить тёмную тему'}
|
||||||
aria-pressed={isDark}
|
aria-pressed={isDark}
|
||||||
onClick={toggleTheme}
|
onClick={(event) => toggleTheme(event.currentTarget)}
|
||||||
>
|
>
|
||||||
<span className="login-theme-switch__track">
|
{isDark ? <Moon size={18} /> : <Sun size={18} />}
|
||||||
<span className="login-theme-switch__icon login-theme-switch__icon--sun">
|
|
||||||
<Sun size={14} />
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span className="login-theme-switch__icon login-theme-switch__icon--moon">
|
|
||||||
<Moon size={14} />
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span className="login-theme-switch__thumb">
|
|
||||||
{isDark ? <Moon size={15} /> : <Sun size={15} />}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<form className="login-card" onSubmit={handleSubmit}>
|
<form className="login-card" onSubmit={handleSubmit}>
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ import {
|
||||||
type MapTrackPeriodValue,
|
type MapTrackPeriodValue,
|
||||||
} from './components/MapTrackPeriodControl/MapTrackPeriodControl'
|
} from './components/MapTrackPeriodControl/MapTrackPeriodControl'
|
||||||
import { popoverVariants, uiFadeTransition } from '../../shared/lib/motion'
|
import { popoverVariants, uiFadeTransition } from '../../shared/lib/motion'
|
||||||
|
import { isPresent } from '../../shared/lib/isPresent'
|
||||||
|
|
||||||
import 'leaflet/dist/leaflet.css'
|
import 'leaflet/dist/leaflet.css'
|
||||||
import './MapPage.scss'
|
import './MapPage.scss'
|
||||||
|
|
@ -51,6 +52,12 @@ type MapInfoPoint = {
|
||||||
lng: number
|
lng: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type MapDevice = GetPhonesData['getPhones']['page'][number]
|
||||||
|
|
||||||
|
type MapDeviceWithLocation = MapDevice & {
|
||||||
|
lastLocation: NonNullable<MapDevice['lastLocation']>
|
||||||
|
}
|
||||||
|
|
||||||
function getNetworkStatusClass(status?: DeviceNetworkStatus) {
|
function getNetworkStatusClass(status?: DeviceNetworkStatus) {
|
||||||
if (status === 'Online') return 'is-online'
|
if (status === 'Online') return 'is-online'
|
||||||
if (status === 'Lost') return 'is-lost'
|
if (status === 'Lost') return 'is-lost'
|
||||||
|
|
@ -348,9 +355,8 @@ export function MapPage() {
|
||||||
|
|
||||||
const devicesWithLocation = useMemo(() => {
|
const devicesWithLocation = useMemo(() => {
|
||||||
return devices.filter(
|
return devices.filter(
|
||||||
(device): device is Device & {
|
(device): device is MapDeviceWithLocation =>
|
||||||
lastLocation: NonNullable<Device['lastLocation']>
|
device.lastLocation !== null,
|
||||||
} => Boolean(device.lastLocation),
|
|
||||||
)
|
)
|
||||||
}, [devices])
|
}, [devices])
|
||||||
const shouldShowPhonesLoadState =
|
const shouldShowPhonesLoadState =
|
||||||
|
|
@ -359,7 +365,9 @@ export function MapPage() {
|
||||||
(isPhonesLoading && devicesWithLocation.length === 0))
|
(isPhonesLoading && devicesWithLocation.length === 0))
|
||||||
|
|
||||||
const sortedGpsTrack = useMemo(() => {
|
const sortedGpsTrack = useMemo(() => {
|
||||||
return sortGpsTrack(gpsTrackData?.getPhoneGpsTrack ?? [])
|
return sortGpsTrack(
|
||||||
|
(gpsTrackData?.getPhoneGpsTrack ?? []).filter(isPresent),
|
||||||
|
)
|
||||||
}, [gpsTrackData])
|
}, [gpsTrackData])
|
||||||
|
|
||||||
const selectedLastLocation = selectedDevice?.lastLocation ?? null
|
const selectedLastLocation = selectedDevice?.lastLocation ?? null
|
||||||
|
|
|
||||||
|
|
@ -604,6 +604,11 @@
|
||||||
background-color: $color-danger-status-bg;
|
background-color: $color-danger-status-bg;
|
||||||
color: $red;
|
color: $red;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.devices-status--blue {
|
||||||
|
background-color: $color-primary-muted;
|
||||||
|
color: $blue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.organisation-device-dot {
|
.organisation-device-dot {
|
||||||
|
|
@ -623,6 +628,10 @@
|
||||||
&.devices-dot--gray {
|
&.devices-dot--gray {
|
||||||
background: $gray50;
|
background: $gray50;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.devices-dot--blue {
|
||||||
|
background: $blue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.organisation-device-icons {
|
.organisation-device-icons {
|
||||||
|
|
@ -1069,4 +1078,4 @@
|
||||||
.organisation-devices-table {
|
.organisation-devices-table {
|
||||||
min-width: 800px;
|
min-width: 800px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,9 +39,14 @@ import type {
|
||||||
import type {
|
import type {
|
||||||
Device as OrganisationDevice,
|
Device as OrganisationDevice,
|
||||||
DeviceNetworkStatus,
|
DeviceNetworkStatus,
|
||||||
|
DeviceTechnicalStatus,
|
||||||
GetPhonesData,
|
GetPhonesData,
|
||||||
GetPhonesVariables,
|
GetPhonesVariables,
|
||||||
} from '../../entities/device/model/types'
|
} from '../../entities/device/model/types'
|
||||||
|
import {
|
||||||
|
getDeviceMalfunctionSummary,
|
||||||
|
deviceTechnicalStatusLabels,
|
||||||
|
} from '../../entities/device/lib/maintenance'
|
||||||
import { ConfirmDangerDialog } from '../../widgets/ConfirmDangerDialog/ConfirmDangerDialog'
|
import { ConfirmDangerDialog } from '../../widgets/ConfirmDangerDialog/ConfirmDangerDialog'
|
||||||
import { EmployeesPagination } from '../EmployeesPage/components/EmployeesPagination/EmployeesPagination'
|
import { EmployeesPagination } from '../EmployeesPage/components/EmployeesPagination/EmployeesPagination'
|
||||||
import {
|
import {
|
||||||
|
|
@ -67,6 +72,7 @@ import {
|
||||||
import { RevealContent } from '../../shared/ui/RevealContent/RevealContent'
|
import { RevealContent } from '../../shared/ui/RevealContent/RevealContent'
|
||||||
import { isPhonePolicyOptionEnabled } from '../../entities/device/lib/phonePolicy'
|
import { isPhonePolicyOptionEnabled } from '../../entities/device/lib/phonePolicy'
|
||||||
import { EmptyState } from '../../shared/ui/EmptyState/EmptyState'
|
import { EmptyState } from '../../shared/ui/EmptyState/EmptyState'
|
||||||
|
import { Tooltip } from '../../shared/ui/Tooltip/Tooltip'
|
||||||
import type { AppLayoutOutletContext } from '../../app/layouts/AppLayout'
|
import type { AppLayoutOutletContext } from '../../app/layouts/AppLayout'
|
||||||
import {
|
import {
|
||||||
useDeferredModalPayload,
|
useDeferredModalPayload,
|
||||||
|
|
@ -108,16 +114,22 @@ function getEmployeeRoleLabel(role: string) {
|
||||||
return role
|
return role
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDeviceConditionLabel(needMaintenance?: boolean) {
|
function getDeviceConditionLabel(status?: DeviceTechnicalStatus) {
|
||||||
return needMaintenance ? 'Требует ТО' : 'Исправно'
|
return deviceTechnicalStatusLabels[status ?? 'Healthy']
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDeviceConditionClass(needMaintenance?: boolean) {
|
function getDeviceConditionClass(status?: DeviceTechnicalStatus) {
|
||||||
return needMaintenance ? 'devices-status--red' : 'devices-status--green'
|
if (status === 'NeedsMaintenance') return 'devices-status--red'
|
||||||
|
if (status === 'InService') return 'devices-status--blue'
|
||||||
|
|
||||||
|
return 'devices-status--green'
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDeviceConditionDotClass(needMaintenance?: boolean) {
|
function getDeviceConditionDotClass(status?: DeviceTechnicalStatus) {
|
||||||
return needMaintenance ? 'devices-dot--red' : 'devices-dot--green'
|
if (status === 'NeedsMaintenance') return 'devices-dot--red'
|
||||||
|
if (status === 'InService') return 'devices-dot--blue'
|
||||||
|
|
||||||
|
return 'devices-dot--green'
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDeviceNetworkLabel(status?: DeviceNetworkStatus) {
|
function getDeviceNetworkLabel(status?: DeviceNetworkStatus) {
|
||||||
|
|
@ -941,21 +953,28 @@ export function OrganisationPage() {
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td>
|
<td>
|
||||||
<div
|
<Tooltip
|
||||||
className={`organisation-device-status ${getDeviceConditionClass(
|
content={getDeviceMalfunctionSummary(
|
||||||
device.techState?.needMaintenance,
|
device.techState?.malfunctions,
|
||||||
)}`}
|
device.techState?.malfunctionComment,
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className={`organisation-device-dot ${getDeviceConditionDotClass(
|
className={`organisation-device-status ${getDeviceConditionClass(
|
||||||
device.techState?.needMaintenance,
|
device.techState?.status,
|
||||||
)}`}
|
)}`}
|
||||||
/>
|
>
|
||||||
|
<span
|
||||||
|
className={`organisation-device-dot ${getDeviceConditionDotClass(
|
||||||
|
device.techState?.status,
|
||||||
|
)}`}
|
||||||
|
/>
|
||||||
|
|
||||||
{getDeviceConditionLabel(
|
{getDeviceConditionLabel(
|
||||||
device.techState?.needMaintenance,
|
device.techState?.status,
|
||||||
)}
|
)}
|
||||||
</div>
|
</span>
|
||||||
|
</Tooltip>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td>
|
<td>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,87 @@
|
||||||
|
/* eslint-disable */
|
||||||
|
import type { ResultOf, DocumentTypeDecoration, TypedDocumentNode } from '@graphql-typed-document-node/core';
|
||||||
|
import type { FragmentDefinitionNode } from 'graphql';
|
||||||
|
import type { Incremental } from './graphql';
|
||||||
|
|
||||||
|
|
||||||
|
export type FragmentType<TDocumentType extends DocumentTypeDecoration<any, any>> = TDocumentType extends DocumentTypeDecoration<
|
||||||
|
infer TType,
|
||||||
|
any
|
||||||
|
>
|
||||||
|
? [TType] extends [{ ' $fragmentName'?: infer TKey }]
|
||||||
|
? TKey extends string
|
||||||
|
? { ' $fragmentRefs'?: { [key in TKey]: TType } }
|
||||||
|
: never
|
||||||
|
: never
|
||||||
|
: never;
|
||||||
|
|
||||||
|
// return non-nullable if `fragmentType` is non-nullable
|
||||||
|
export function useFragment<TType>(
|
||||||
|
_documentNode: DocumentTypeDecoration<TType, any>,
|
||||||
|
fragmentType: FragmentType<DocumentTypeDecoration<TType, any>>
|
||||||
|
): TType;
|
||||||
|
// return nullable if `fragmentType` is undefined
|
||||||
|
export function useFragment<TType>(
|
||||||
|
_documentNode: DocumentTypeDecoration<TType, any>,
|
||||||
|
fragmentType: FragmentType<DocumentTypeDecoration<TType, any>> | undefined
|
||||||
|
): TType | undefined;
|
||||||
|
// return nullable if `fragmentType` is nullable
|
||||||
|
export function useFragment<TType>(
|
||||||
|
_documentNode: DocumentTypeDecoration<TType, any>,
|
||||||
|
fragmentType: FragmentType<DocumentTypeDecoration<TType, any>> | null
|
||||||
|
): TType | null;
|
||||||
|
// return nullable if `fragmentType` is nullable or undefined
|
||||||
|
export function useFragment<TType>(
|
||||||
|
_documentNode: DocumentTypeDecoration<TType, any>,
|
||||||
|
fragmentType: FragmentType<DocumentTypeDecoration<TType, any>> | null | undefined
|
||||||
|
): TType | null | undefined;
|
||||||
|
// return array of non-nullable if `fragmentType` is array of non-nullable
|
||||||
|
export function useFragment<TType>(
|
||||||
|
_documentNode: DocumentTypeDecoration<TType, any>,
|
||||||
|
fragmentType: Array<FragmentType<DocumentTypeDecoration<TType, any>>>
|
||||||
|
): Array<TType>;
|
||||||
|
// return array of nullable if `fragmentType` is array of nullable
|
||||||
|
export function useFragment<TType>(
|
||||||
|
_documentNode: DocumentTypeDecoration<TType, any>,
|
||||||
|
fragmentType: Array<FragmentType<DocumentTypeDecoration<TType, any>>> | null | undefined
|
||||||
|
): Array<TType> | null | undefined;
|
||||||
|
// return readonly array of non-nullable if `fragmentType` is array of non-nullable
|
||||||
|
export function useFragment<TType>(
|
||||||
|
_documentNode: DocumentTypeDecoration<TType, any>,
|
||||||
|
fragmentType: ReadonlyArray<FragmentType<DocumentTypeDecoration<TType, any>>>
|
||||||
|
): ReadonlyArray<TType>;
|
||||||
|
// return readonly array of nullable if `fragmentType` is array of nullable
|
||||||
|
export function useFragment<TType>(
|
||||||
|
_documentNode: DocumentTypeDecoration<TType, any>,
|
||||||
|
fragmentType: ReadonlyArray<FragmentType<DocumentTypeDecoration<TType, any>>> | null | undefined
|
||||||
|
): ReadonlyArray<TType> | null | undefined;
|
||||||
|
export function useFragment<TType>(
|
||||||
|
_documentNode: DocumentTypeDecoration<TType, any>,
|
||||||
|
fragmentType: FragmentType<DocumentTypeDecoration<TType, any>> | Array<FragmentType<DocumentTypeDecoration<TType, any>>> | ReadonlyArray<FragmentType<DocumentTypeDecoration<TType, any>>> | null | undefined
|
||||||
|
): TType | Array<TType> | ReadonlyArray<TType> | null | undefined {
|
||||||
|
return fragmentType as any;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function makeFragmentData<
|
||||||
|
F extends DocumentTypeDecoration<any, any>,
|
||||||
|
FT extends ResultOf<F>
|
||||||
|
>(data: FT, _fragment: F): FragmentType<F> {
|
||||||
|
return data as FragmentType<F>;
|
||||||
|
}
|
||||||
|
export function isFragmentReady<TQuery, TFrag>(
|
||||||
|
queryNode: DocumentTypeDecoration<TQuery, any>,
|
||||||
|
fragmentNode: TypedDocumentNode<TFrag>,
|
||||||
|
data: FragmentType<TypedDocumentNode<Incremental<TFrag>, any>> | null | undefined
|
||||||
|
): data is FragmentType<typeof fragmentNode> {
|
||||||
|
const deferredFields = (queryNode as { __meta__?: { deferredFields: Record<string, (keyof TFrag)[]> } }).__meta__
|
||||||
|
?.deferredFields;
|
||||||
|
|
||||||
|
if (!deferredFields) return true;
|
||||||
|
|
||||||
|
const fragDef = fragmentNode.definitions[0] as FragmentDefinitionNode | undefined;
|
||||||
|
const fragName = fragDef?.name?.value;
|
||||||
|
|
||||||
|
const fields = (fragName && deferredFields[fragName]) || [];
|
||||||
|
return fields.length > 0 && fields.every(field => data && field in data);
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,2 @@
|
||||||
|
export * from "./fragment-masking";
|
||||||
|
export * from "./gql";
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
export function isPresent<T>(
|
||||||
|
value: T | null | undefined,
|
||||||
|
): value is T {
|
||||||
|
return value !== null && value !== undefined
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,112 @@
|
||||||
|
import type { AppTheme } from './theme'
|
||||||
|
|
||||||
|
type ViewTransition = {
|
||||||
|
finished: Promise<void>
|
||||||
|
ready: Promise<void>
|
||||||
|
skipTransition: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
type ViewTransitionDocument = Document & {
|
||||||
|
startViewTransition?: (callback: () => void) => ViewTransition
|
||||||
|
}
|
||||||
|
|
||||||
|
type AnimationOptionsWithPseudoElement = KeyframeAnimationOptions & {
|
||||||
|
pseudoElement?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ThemeTransitionOptions = {
|
||||||
|
source?: Element | null
|
||||||
|
theme: AppTheme
|
||||||
|
updateTheme: (theme: AppTheme) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const THEME_TRANSITION_CLASS = 'theme-transition-no-css-transitions'
|
||||||
|
const THEME_TRANSITION_DURATION = 620
|
||||||
|
const THEME_TRANSITION_EASING = 'cubic-bezier(0.22, 1, 0.36, 1)'
|
||||||
|
|
||||||
|
let activeTransition: ViewTransition | null = null
|
||||||
|
|
||||||
|
function shouldReduceMotion() {
|
||||||
|
return window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTransitionGeometry(source?: Element | null) {
|
||||||
|
const rect = source?.getBoundingClientRect()
|
||||||
|
const x = rect ? rect.left + rect.width / 2 : window.innerWidth - 48
|
||||||
|
const y = rect ? rect.top + rect.height / 2 : 48
|
||||||
|
const endRadius = Math.hypot(
|
||||||
|
Math.max(x, window.innerWidth - x),
|
||||||
|
Math.max(y, window.innerHeight - y),
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
endRadius,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateThemeWithTransition({
|
||||||
|
source,
|
||||||
|
theme,
|
||||||
|
updateTheme,
|
||||||
|
}: ThemeTransitionOptions) {
|
||||||
|
if (typeof document === 'undefined' || typeof window === 'undefined') {
|
||||||
|
updateTheme(theme)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const transitionDocument = document as ViewTransitionDocument
|
||||||
|
|
||||||
|
if (!transitionDocument.startViewTransition || shouldReduceMotion()) {
|
||||||
|
updateTheme(theme)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
activeTransition?.skipTransition()
|
||||||
|
|
||||||
|
const root = document.documentElement
|
||||||
|
const { endRadius, x, y } = getTransitionGeometry(source)
|
||||||
|
|
||||||
|
root.classList.add(THEME_TRANSITION_CLASS)
|
||||||
|
|
||||||
|
const transition = transitionDocument.startViewTransition(() => {
|
||||||
|
updateTheme(theme)
|
||||||
|
})
|
||||||
|
|
||||||
|
activeTransition = transition
|
||||||
|
|
||||||
|
transition.ready
|
||||||
|
.then(() => {
|
||||||
|
root.classList.remove(THEME_TRANSITION_CLASS)
|
||||||
|
|
||||||
|
root.animate(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
clipPath: `circle(0px at ${x}px ${y}px)`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
clipPath: `circle(${endRadius}px at ${x}px ${y}px)`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{
|
||||||
|
duration: THEME_TRANSITION_DURATION,
|
||||||
|
easing: THEME_TRANSITION_EASING,
|
||||||
|
pseudoElement: '::view-transition-new(root)',
|
||||||
|
} as AnimationOptionsWithPseudoElement,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
root.classList.remove(THEME_TRANSITION_CLASS)
|
||||||
|
})
|
||||||
|
|
||||||
|
transition.finished
|
||||||
|
.catch(() => undefined)
|
||||||
|
.finally(() => {
|
||||||
|
root.classList.remove(THEME_TRANSITION_CLASS)
|
||||||
|
|
||||||
|
if (activeTransition === transition) {
|
||||||
|
activeTransition = null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
|
import { flushSync } from 'react-dom'
|
||||||
import type { AppTheme } from './theme'
|
import type { AppTheme } from './theme'
|
||||||
import {
|
import {
|
||||||
applyTheme,
|
applyTheme,
|
||||||
|
|
@ -6,6 +7,7 @@ import {
|
||||||
getThemeStorageKey,
|
getThemeStorageKey,
|
||||||
saveTheme,
|
saveTheme,
|
||||||
} from './theme'
|
} from './theme'
|
||||||
|
import { updateThemeWithTransition } from './themeTransition'
|
||||||
|
|
||||||
export function useTheme() {
|
export function useTheme() {
|
||||||
const [theme, setTheme] = useState<AppTheme>(() => getPreferredTheme())
|
const [theme, setTheme] = useState<AppTheme>(() => getPreferredTheme())
|
||||||
|
|
@ -30,12 +32,22 @@ export function useTheme() {
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
function toggleTheme() {
|
function commitTheme(nextTheme: AppTheme) {
|
||||||
const nextTheme = theme === 'dark' ? 'light' : 'dark'
|
|
||||||
|
|
||||||
applyTheme(nextTheme)
|
applyTheme(nextTheme)
|
||||||
saveTheme(nextTheme)
|
saveTheme(nextTheme)
|
||||||
setTheme(nextTheme)
|
flushSync(() => {
|
||||||
|
setTheme(nextTheme)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleTheme(source?: Element | null) {
|
||||||
|
const nextTheme = theme === 'dark' ? 'light' : 'dark'
|
||||||
|
|
||||||
|
updateThemeWithTransition({
|
||||||
|
source,
|
||||||
|
theme: nextTheme,
|
||||||
|
updateTheme: commitTheme,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
@use '../../styles/variables' as *;
|
||||||
|
|
||||||
|
.ui-tooltip {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
min-width: 0;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-tooltip__content {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 1000;
|
||||||
|
width: max-content;
|
||||||
|
max-width: min(320px, calc(100vw - 32px));
|
||||||
|
padding: 8px 10px;
|
||||||
|
border: 1px solid $color-border-muted;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: $color-surface;
|
||||||
|
color: $color-text-strong;
|
||||||
|
box-shadow: $shadow-popover;
|
||||||
|
font-size: $font-size-12;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1.4;
|
||||||
|
text-align: left;
|
||||||
|
white-space: normal;
|
||||||
|
pointer-events: none;
|
||||||
|
transform: translate(-50%, -100%);
|
||||||
|
animation: ui-tooltip-in 0.18s ease both;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: calc(100% - 1px);
|
||||||
|
left: 50%;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-right: 1px solid $color-border-muted;
|
||||||
|
border-bottom: 1px solid $color-border-muted;
|
||||||
|
background: $color-surface;
|
||||||
|
transform: translate(-50%, -4px) rotate(45deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes ui-tooltip-in {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translate(-50%, calc(-100% + 4px));
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translate(-50%, -100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,84 @@
|
||||||
|
import {
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useId,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
type ReactNode,
|
||||||
|
} from 'react'
|
||||||
|
import { createPortal } from 'react-dom'
|
||||||
|
|
||||||
|
import './Tooltip.scss'
|
||||||
|
|
||||||
|
type TooltipProps = {
|
||||||
|
children: ReactNode
|
||||||
|
content?: ReactNode
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Tooltip({ children, content, className = '' }: TooltipProps) {
|
||||||
|
const triggerRef = useRef<HTMLSpanElement>(null)
|
||||||
|
const [isOpen, setIsOpen] = useState(false)
|
||||||
|
const [position, setPosition] = useState({ left: 0, top: 0 })
|
||||||
|
const tooltipId = useId()
|
||||||
|
|
||||||
|
const updatePosition = useCallback(() => {
|
||||||
|
const trigger = triggerRef.current
|
||||||
|
if (!trigger) return
|
||||||
|
|
||||||
|
const rect = trigger.getBoundingClientRect()
|
||||||
|
setPosition({
|
||||||
|
left: rect.left + rect.width / 2,
|
||||||
|
top: rect.top - 9,
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const openTooltip = useCallback(() => {
|
||||||
|
updatePosition()
|
||||||
|
setIsOpen(true)
|
||||||
|
}, [updatePosition])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isOpen) return
|
||||||
|
|
||||||
|
const handleViewportChange = () => updatePosition()
|
||||||
|
window.addEventListener('resize', handleViewportChange)
|
||||||
|
window.addEventListener('scroll', handleViewportChange, true)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('resize', handleViewportChange)
|
||||||
|
window.removeEventListener('scroll', handleViewportChange, true)
|
||||||
|
}
|
||||||
|
}, [isOpen, updatePosition])
|
||||||
|
|
||||||
|
if (!content) return <>{children}</>
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<span
|
||||||
|
ref={triggerRef}
|
||||||
|
className={`ui-tooltip ${className}`}
|
||||||
|
tabIndex={0}
|
||||||
|
aria-describedby={isOpen ? tooltipId : undefined}
|
||||||
|
onMouseEnter={openTooltip}
|
||||||
|
onMouseLeave={() => setIsOpen(false)}
|
||||||
|
onFocus={openTooltip}
|
||||||
|
onBlur={() => setIsOpen(false)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{isOpen && typeof document !== 'undefined' && createPortal(
|
||||||
|
<span
|
||||||
|
id={tooltipId}
|
||||||
|
className="ui-tooltip__content"
|
||||||
|
role="tooltip"
|
||||||
|
style={position}
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
</span>,
|
||||||
|
document.body,
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -169,166 +169,37 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar__theme-btn {
|
.navbar__theme-button {
|
||||||
color: $color-primary;
|
width: 32px;
|
||||||
|
|
||||||
&[aria-pressed='true'] {
|
|
||||||
background: $color-surface;
|
|
||||||
box-shadow: $shadow-control;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar__theme-switch {
|
|
||||||
width: 72px;
|
|
||||||
height: 32px;
|
height: 32px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 999px;
|
border-radius: 12px;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
|
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
||||||
|
color: $gray50;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
transition: 0.2s ease;
|
||||||
|
|
||||||
.navbar__theme-switch-track {
|
svg {
|
||||||
position: relative;
|
flex: 0 0 auto;
|
||||||
width: 62px;
|
transition: transform 0.2s ease;
|
||||||
height: 26px;
|
|
||||||
padding: 4px;
|
|
||||||
border-radius: 999px;
|
|
||||||
|
|
||||||
background: $color-surface;
|
|
||||||
box-shadow:
|
|
||||||
inset 0 0 1px 1px rgba(var(--gray50-rgb), 0.3),
|
|
||||||
0 6px 20px -6px rgba(var(--gray50-rgb), 0.3);
|
|
||||||
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
transition:
|
|
||||||
background 0.25s ease,
|
|
||||||
box-shadow 0.25s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar__theme-switch-track::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
border-radius: inherit;
|
|
||||||
|
|
||||||
background:
|
|
||||||
radial-gradient(circle at 25% 35%, rgba($orange, 0.2), transparent 32%),
|
|
||||||
radial-gradient(circle at 78% 65%, rgba($blue, 0.18), transparent 36%);
|
|
||||||
|
|
||||||
opacity: 0.9;
|
|
||||||
transition: opacity 0.25s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar__theme-switch-thumb {
|
|
||||||
position: relative;
|
|
||||||
z-index: 2;
|
|
||||||
|
|
||||||
width: 26px;
|
|
||||||
height: 26px;
|
|
||||||
border-radius: 50%;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
background: #ffffff;
|
|
||||||
color: $orange;
|
|
||||||
|
|
||||||
box-shadow: 0px 1px 8px -2px rgba(var(--gray50-rgb), 0.8);
|
|
||||||
|
|
||||||
transform: translateX(0);
|
|
||||||
|
|
||||||
transition:
|
|
||||||
transform 0.28s cubic-bezier(0.34, 1.56, 0.64, 1),
|
|
||||||
background 0.25s ease,
|
|
||||||
color 0.25s ease,
|
|
||||||
box-shadow 0.25s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar__theme-switch-icon {
|
|
||||||
position: absolute;
|
|
||||||
z-index: 1;
|
|
||||||
top: 50%;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
color: $color-text-muted;
|
|
||||||
opacity: 0.55;
|
|
||||||
|
|
||||||
transform: translateY(-50%);
|
|
||||||
|
|
||||||
transition:
|
|
||||||
opacity 0.25s ease,
|
|
||||||
color 0.25s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar__theme-switch-icon--sun {
|
|
||||||
left: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar__theme-switch-icon--moon {
|
|
||||||
right: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar__theme-switch.is-light {
|
|
||||||
.navbar__theme-switch-icon--sun {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar__theme-switch-icon--moon {
|
&:hover {
|
||||||
opacity: 0.45;
|
background: $gray30;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar__theme-switch.is-dark {
|
|
||||||
.navbar__theme-switch-track {
|
|
||||||
background: $color-surface;
|
|
||||||
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.08), 0 8px 22px rgba(0, 0, 0, 0.22);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar__theme-switch-track::before {
|
&:active {
|
||||||
opacity: 0.65;
|
transform: scale(0.96);
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar__theme-switch-thumb {
|
&.is-dark:hover {
|
||||||
transform: translateX(36px);
|
background: var(--color-surface-hover);
|
||||||
background: $color-surface;
|
|
||||||
color: $blue;
|
|
||||||
|
|
||||||
box-shadow: 0px 1px 6px -2px $gray50;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar__theme-switch-icon--sun {
|
|
||||||
opacity: 0.38;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar__theme-switch-icon--moon {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar__theme-switch:hover {
|
|
||||||
.navbar__theme-switch-track {
|
|
||||||
//box-shadow: inset 0 0 0 1px rgba($blue, 0.35), 0 8px 20px rgba($blue, 0.12);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar__theme-switch:active {
|
|
||||||
.navbar__theme-switch-thumb {
|
|
||||||
transform: scale(0.94);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.is-dark .navbar__theme-switch-thumb {
|
|
||||||
transform: translateX(24px) scale(0.94);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -520,4 +391,4 @@
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: translateY(0) scale(1);
|
transform: translateY(0) scale(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import {
|
||||||
useDeferredModalPayload,
|
useDeferredModalPayload,
|
||||||
} from '../../shared/lib/lazyMount'
|
} from '../../shared/lib/lazyMount'
|
||||||
import { AddEmployeeModal } from '../../pages/EmployeesPage/components/AddEmployeeModal/AddEmployeeModal'
|
import { AddEmployeeModal } from '../../pages/EmployeesPage/components/AddEmployeeModal/AddEmployeeModal'
|
||||||
|
import type { CurrentUserQuery } from '../../shared/api/generated/graphql'
|
||||||
|
|
||||||
function clearAuthCookies() {
|
function clearAuthCookies() {
|
||||||
document.cookie = 'Access-token=; Max-Age=0; path=/'
|
document.cookie = 'Access-token=; Max-Age=0; path=/'
|
||||||
|
|
@ -24,22 +25,6 @@ async function handleLogout() {
|
||||||
window.dispatchEvent(new Event('auth:logout'))
|
window.dispatchEvent(new Event('auth:logout'))
|
||||||
}
|
}
|
||||||
|
|
||||||
type CurrentUserData = {
|
|
||||||
currentUser: {
|
|
||||||
id: string
|
|
||||||
role: string
|
|
||||||
avatarUrl: string
|
|
||||||
firstName: string
|
|
||||||
middleName: string
|
|
||||||
lastName: string
|
|
||||||
username: string
|
|
||||||
org: {
|
|
||||||
id: number
|
|
||||||
name: string
|
|
||||||
} | null
|
|
||||||
} | null
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPageTitle(location: ReturnType<typeof useLocation>) {
|
function getPageTitle(location: ReturnType<typeof useLocation>) {
|
||||||
const { pathname, search } = location
|
const { pathname, search } = location
|
||||||
const searchParams = new URLSearchParams(search)
|
const searchParams = new URLSearchParams(search)
|
||||||
|
|
@ -76,10 +61,10 @@ function getPageTitleLink(location: ReturnType<typeof useLocation>) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapCurrentUserToEmployee(
|
function mapCurrentUserToEmployee(
|
||||||
user: NonNullable<CurrentUserData['currentUser']>,
|
user: CurrentUserQuery['currentUser'],
|
||||||
): Employee {
|
): Employee {
|
||||||
return {
|
return {
|
||||||
id: Number(user.id),
|
id: user.id,
|
||||||
avatarUrl: user.avatarUrl,
|
avatarUrl: user.avatarUrl,
|
||||||
firstName: user.firstName,
|
firstName: user.firstName,
|
||||||
lastName: user.lastName,
|
lastName: user.lastName,
|
||||||
|
|
@ -109,7 +94,7 @@ export function Navbar({
|
||||||
}: NavbarProps) {
|
}: NavbarProps) {
|
||||||
const { isDark, toggleTheme } = useTheme()
|
const { isDark, toggleTheme } = useTheme()
|
||||||
|
|
||||||
const { data, refetch } = useQuery<CurrentUserData>(CURRENT_USER_QUERY, {
|
const { data, refetch } = useQuery(CURRENT_USER_QUERY, {
|
||||||
fetchPolicy: 'cache-first',
|
fetchPolicy: 'cache-first',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -180,25 +165,13 @@ export function Navbar({
|
||||||
</button> */}
|
</button> */}
|
||||||
|
|
||||||
<button
|
<button
|
||||||
className={`navbar__theme-switch ${isDark ? 'is-dark' : 'is-light'}`}
|
className={`navbar__theme-button ${isDark ? 'is-dark' : 'is-light'}`}
|
||||||
type="button"
|
type="button"
|
||||||
aria-label={isDark ? 'Включить светлую тему' : 'Включить тёмную тему'}
|
aria-label={isDark ? 'Включить светлую тему' : 'Включить тёмную тему'}
|
||||||
aria-pressed={isDark}
|
aria-pressed={isDark}
|
||||||
onClick={toggleTheme}
|
onClick={(event) => toggleTheme(event.currentTarget)}
|
||||||
>
|
>
|
||||||
<span className="navbar__theme-switch-track">
|
{isDark ? <Moon size={17} /> : <Sun size={17} />}
|
||||||
<span className="navbar__theme-switch-icon navbar__theme-switch-icon--sun">
|
|
||||||
<Sun size={14} />
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span className="navbar__theme-switch-icon navbar__theme-switch-icon--moon">
|
|
||||||
<Moon size={14} />
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span className="navbar__theme-switch-thumb">
|
|
||||||
{isDark ? <Moon size={15} /> : <Sun size={15} />}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<DropdownMenu.Root>
|
<DropdownMenu.Root>
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,9 @@
|
||||||
|
|
||||||
.notification {
|
.notification {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
right: 24px;
|
right: max(24px, env(safe-area-inset-right));
|
||||||
bottom: 24px;
|
bottom: max(24px, env(safe-area-inset-bottom));
|
||||||
z-index: 300;
|
z-index: 1200;
|
||||||
|
|
||||||
width: min(520px, calc(100vw - 32px));
|
width: min(520px, calc(100vw - 32px));
|
||||||
//min-height: 76px;
|
//min-height: 76px;
|
||||||
|
|
@ -21,7 +21,7 @@
|
||||||
|
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateY(14px) scale(0.98);
|
transform: translateY(14px) scale(0.98);
|
||||||
pointer-events: none;
|
pointer-events: auto;
|
||||||
transition:
|
transition:
|
||||||
opacity 0.22s ease,
|
opacity 0.22s ease,
|
||||||
transform 0.22s ease;
|
transform 0.22s ease;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { useCallback, useEffect, useState } from 'react'
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
|
import { createPortal } from 'react-dom'
|
||||||
import { CheckCircle2, X, XCircle } from 'lucide-react'
|
import { CheckCircle2, X, XCircle } from 'lucide-react'
|
||||||
import { m } from 'framer-motion'
|
import { m } from 'framer-motion'
|
||||||
|
|
||||||
|
|
@ -49,7 +50,9 @@ export function Notification({
|
||||||
|
|
||||||
const Icon = variant === 'success' ? CheckCircle2 : XCircle
|
const Icon = variant === 'success' ? CheckCircle2 : XCircle
|
||||||
|
|
||||||
return (
|
if (typeof document === 'undefined') return null
|
||||||
|
|
||||||
|
return createPortal(
|
||||||
<m.div
|
<m.div
|
||||||
className={`notification notification--${variant}`}
|
className={`notification notification--${variant}`}
|
||||||
role="status"
|
role="status"
|
||||||
|
|
@ -92,6 +95,7 @@ export function Notification({
|
||||||
>
|
>
|
||||||
<X size={20} />
|
<X size={20} />
|
||||||
</button>
|
</button>
|
||||||
</m.div>
|
</m.div>,
|
||||||
|
document.body,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,20 +9,20 @@ export default defineConfig({
|
||||||
host: true,
|
host: true,
|
||||||
proxy: {
|
proxy: {
|
||||||
'/graphql': {
|
'/graphql': {
|
||||||
target: 'http://192.168.1.179:8080',
|
target: 'http://192.168.1.91:8080',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
secure: false,
|
secure: false,
|
||||||
},
|
},
|
||||||
'/user': {
|
'/user': {
|
||||||
target: 'http://192.168.1.179:8080',
|
target: 'http://192.168.1.91:8080',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
},
|
},
|
||||||
'/org': {
|
'/org': {
|
||||||
target: 'http://192.168.1.179:8080',
|
target: 'http://192.168.1.91:8080',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
},
|
},
|
||||||
'/phone': {
|
'/phone': {
|
||||||
target: 'http://192.168.1.179:8080',
|
target: 'http://192.168.1.91:8080',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue